diff --git a/pkg/core/native/audit_logger.go b/pkg/core/native/audit_logger.go new file mode 100644 index 0000000..39a5dd0 --- /dev/null +++ b/pkg/core/native/audit_logger.go @@ -0,0 +1,776 @@ +package native + +import ( + "encoding/binary" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/core/storage" + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// ARCH-005: Comprehensive Audit Logging +// Provides structured audit logging for all sensitive operations, +// supporting compliance requirements, incident investigation, and +// security monitoring. + +// AuditCategory categorizes audit events. +type AuditCategory uint8 + +const ( + AuditCategoryAuth AuditCategory = iota + AuditCategoryAccess + AuditCategoryData + AuditCategoryGovernance + AuditCategoryFinancial + AuditCategorySecurity + AuditCategorySystem + AuditCategoryCompliance +) + +// AuditSeverity indicates the importance of an audit event. +type AuditSeverity uint8 + +const ( + AuditSeverityInfo AuditSeverity = iota + AuditSeverityNotice + AuditSeverityWarning + AuditSeverityAlert + AuditSeverityCritical +) + +// AuditOutcome indicates the result of an audited operation. +type AuditOutcome uint8 + +const ( + AuditOutcomeSuccess AuditOutcome = iota + AuditOutcomeFailure + AuditOutcomeDenied + AuditOutcomeError + AuditOutcomePartial +) + +// AuditEntry represents a single audit log entry. +type AuditEntry struct { + // EntryID is unique identifier for this entry + EntryID uint64 + // Timestamp is the block height when event occurred + Timestamp uint32 + // Category classifies the event + Category AuditCategory + // Severity indicates importance + Severity AuditSeverity + // Outcome is the result of the operation + Outcome AuditOutcome + // ContractID identifies the originating contract + ContractID int32 + // Actor is who performed the action + Actor util.Uint160 + // Target is the subject of the action (if applicable) + Target util.Uint160 + // Action is the operation performed + Action string + // ResourceID identifies the affected resource + ResourceID []byte + // Details contains additional context + Details string + // IPHash is hash of source IP (for off-chain correlation) + IPHash util.Uint256 + // PreviousState is hash of state before change + PreviousState util.Uint256 + // NewState is hash of state after change + NewState util.Uint256 +} + +// AuditQuery defines parameters for searching audit logs. +type AuditQuery struct { + // StartBlock is the earliest block to search + StartBlock uint32 + // EndBlock is the latest block to search + EndBlock uint32 + // Category filters by event category (nil = all) + Category *AuditCategory + // Severity filters by minimum severity + MinSeverity *AuditSeverity + // Actor filters by actor address + Actor *util.Uint160 + // Target filters by target address + Target *util.Uint160 + // ContractID filters by contract + ContractID *int32 + // Limit is maximum results to return + Limit int + // Offset is the starting position + Offset int +} + +// Storage prefixes for audit logging. +const ( + auditPrefixEntry byte = 0xA0 // entryID -> AuditEntry + auditPrefixByBlock byte = 0xA1 // block + entryID -> exists + auditPrefixByActor byte = 0xA2 // actor + block + entryID -> exists + auditPrefixByTarget byte = 0xA3 // target + block + entryID -> exists + auditPrefixByCategory byte = 0xA4 // category + block + entryID -> exists + auditPrefixBySeverity byte = 0xA5 // severity + block + entryID -> exists + auditPrefixByContract byte = 0xA6 // contractID + block + entryID -> exists + auditPrefixCounter byte = 0xAF // -> next entryID + auditPrefixRetention byte = 0xAE // -> retention config +) + +// AuditLogger provides comprehensive audit logging. +type AuditLogger struct { + contractID int32 +} + +// NewAuditLogger creates a new audit logger. +func NewAuditLogger(contractID int32) *AuditLogger { + return &AuditLogger{contractID: contractID} +} + +// Log records an audit entry. +func (al *AuditLogger) Log(d *dao.Simple, entry *AuditEntry) uint64 { + // Get and increment entry counter + entry.EntryID = al.getNextEntryID(d) + + // Store main entry + al.putEntry(d, entry) + + // Create indices for efficient querying + al.indexEntry(d, entry) + + return entry.EntryID +} + +// LogAuth logs an authentication event. +func (al *AuditLogger) LogAuth(d *dao.Simple, actor util.Uint160, action string, outcome AuditOutcome, + blockHeight uint32, details string) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryAuth, + Severity: al.severityForOutcome(outcome, AuditCategoryAuth), + Outcome: outcome, + Actor: actor, + Action: action, + Details: details, + }) +} + +// LogAccess logs a data access event. +func (al *AuditLogger) LogAccess(d *dao.Simple, actor, target util.Uint160, action string, + resourceID []byte, outcome AuditOutcome, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryAccess, + Severity: al.severityForOutcome(outcome, AuditCategoryAccess), + Outcome: outcome, + Actor: actor, + Target: target, + Action: action, + ResourceID: resourceID, + }) +} + +// LogDataChange logs a data modification event. +func (al *AuditLogger) LogDataChange(d *dao.Simple, actor util.Uint160, contractID int32, + action string, resourceID []byte, prevState, newState util.Uint256, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryData, + Severity: AuditSeverityNotice, + Outcome: AuditOutcomeSuccess, + ContractID: contractID, + Actor: actor, + Action: action, + ResourceID: resourceID, + PreviousState: prevState, + NewState: newState, + }) +} + +// LogGovernance logs a governance action. +func (al *AuditLogger) LogGovernance(d *dao.Simple, actor util.Uint160, action string, + details string, outcome AuditOutcome, blockHeight uint32) uint64 { + severity := AuditSeverityNotice + if outcome == AuditOutcomeSuccess { + severity = AuditSeverityWarning // Governance changes warrant attention + } + + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryGovernance, + Severity: severity, + Outcome: outcome, + Actor: actor, + Action: action, + Details: details, + }) +} + +// LogFinancial logs a financial transaction. +func (al *AuditLogger) LogFinancial(d *dao.Simple, actor, target util.Uint160, + action string, resourceID []byte, outcome AuditOutcome, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryFinancial, + Severity: AuditSeverityNotice, + Outcome: outcome, + Actor: actor, + Target: target, + Action: action, + ResourceID: resourceID, + }) +} + +// LogSecurity logs a security-related event. +func (al *AuditLogger) LogSecurity(d *dao.Simple, actor util.Uint160, action string, + severity AuditSeverity, outcome AuditOutcome, details string, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategorySecurity, + Severity: severity, + Outcome: outcome, + Actor: actor, + Action: action, + Details: details, + }) +} + +// LogCompliance logs a compliance-related event. +func (al *AuditLogger) LogCompliance(d *dao.Simple, actor, target util.Uint160, + action string, details string, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryCompliance, + Severity: AuditSeverityNotice, + Outcome: AuditOutcomeSuccess, + Actor: actor, + Target: target, + Action: action, + Details: details, + }) +} + +// LogRightsAccess logs access to rights-protected resources. +func (al *AuditLogger) LogRightsAccess(d *dao.Simple, actor, subject util.Uint160, + rightID uint8, action string, outcome AuditOutcome, blockHeight uint32) uint64 { + return al.Log(d, &AuditEntry{ + Timestamp: blockHeight, + Category: AuditCategoryCompliance, + Severity: AuditSeverityWarning, + Outcome: outcome, + Actor: actor, + Target: subject, + Action: action, + ResourceID: []byte{rightID}, + }) +} + +// GetEntry retrieves an audit entry by ID. +func (al *AuditLogger) GetEntry(d *dao.Simple, entryID uint64) *AuditEntry { + key := al.makeEntryKey(entryID) + si := d.GetStorageItem(al.contractID, key) + if si == nil { + return nil + } + return al.deserializeEntry(si) +} + +// Query searches audit logs based on query parameters. +func (al *AuditLogger) Query(d *dao.Simple, query *AuditQuery) []*AuditEntry { + var entries []*AuditEntry + var entryIDs []uint64 + + // Choose the most selective index + if query.Actor != nil { + entryIDs = al.queryByActor(d, *query.Actor, query.StartBlock, query.EndBlock, query.Limit+query.Offset) + } else if query.Target != nil { + entryIDs = al.queryByTarget(d, *query.Target, query.StartBlock, query.EndBlock, query.Limit+query.Offset) + } else if query.Category != nil { + entryIDs = al.queryByCategory(d, *query.Category, query.StartBlock, query.EndBlock, query.Limit+query.Offset) + } else if query.MinSeverity != nil { + entryIDs = al.queryBySeverity(d, *query.MinSeverity, query.StartBlock, query.EndBlock, query.Limit+query.Offset) + } else { + entryIDs = al.queryByBlock(d, query.StartBlock, query.EndBlock, query.Limit+query.Offset) + } + + // Apply offset and limit + start := query.Offset + if start > len(entryIDs) { + return entries + } + end := start + query.Limit + if end > len(entryIDs) { + end = len(entryIDs) + } + + // Fetch entries + for _, id := range entryIDs[start:end] { + if entry := al.GetEntry(d, id); entry != nil { + // Apply additional filters + if al.matchesQuery(entry, query) { + entries = append(entries, entry) + } + } + } + + return entries +} + +// GetEntriesForActor retrieves audit entries for a specific actor. +func (al *AuditLogger) GetEntriesForActor(d *dao.Simple, actor util.Uint160, limit int) []*AuditEntry { + return al.Query(d, &AuditQuery{ + Actor: &actor, + Limit: limit, + }) +} + +// GetEntriesForTarget retrieves audit entries for a specific target. +func (al *AuditLogger) GetEntriesForTarget(d *dao.Simple, target util.Uint160, limit int) []*AuditEntry { + return al.Query(d, &AuditQuery{ + Target: &target, + Limit: limit, + }) +} + +// GetSecurityAlerts retrieves high-severity security events. +func (al *AuditLogger) GetSecurityAlerts(d *dao.Simple, startBlock, endBlock uint32, limit int) []*AuditEntry { + severity := AuditSeverityAlert + category := AuditCategorySecurity + return al.Query(d, &AuditQuery{ + StartBlock: startBlock, + EndBlock: endBlock, + Category: &category, + MinSeverity: &severity, + Limit: limit, + }) +} + +// GetComplianceLog retrieves compliance-related entries for reporting. +func (al *AuditLogger) GetComplianceLog(d *dao.Simple, startBlock, endBlock uint32, limit int) []*AuditEntry { + category := AuditCategoryCompliance + return al.Query(d, &AuditQuery{ + StartBlock: startBlock, + EndBlock: endBlock, + Category: &category, + Limit: limit, + }) +} + +// severityForOutcome determines severity based on outcome and category. +func (al *AuditLogger) severityForOutcome(outcome AuditOutcome, category AuditCategory) AuditSeverity { + switch outcome { + case AuditOutcomeSuccess: + return AuditSeverityInfo + case AuditOutcomeFailure: + return AuditSeverityNotice + case AuditOutcomeDenied: + if category == AuditCategorySecurity || category == AuditCategoryAuth { + return AuditSeverityAlert + } + return AuditSeverityWarning + case AuditOutcomeError: + return AuditSeverityWarning + default: + return AuditSeverityInfo + } +} + +// matchesQuery checks if an entry matches additional query filters. +func (al *AuditLogger) matchesQuery(entry *AuditEntry, query *AuditQuery) bool { + if query.Category != nil && entry.Category != *query.Category { + return false + } + if query.MinSeverity != nil && entry.Severity < *query.MinSeverity { + return false + } + if query.ContractID != nil && entry.ContractID != *query.ContractID { + return false + } + if query.Actor != nil && entry.Actor != *query.Actor { + return false + } + if query.Target != nil && entry.Target != *query.Target { + return false + } + return true +} + +// Index query methods. +func (al *AuditLogger) queryByBlock(d *dao.Simple, startBlock, endBlock uint32, limit int) []uint64 { + var ids []uint64 + prefix := []byte{auditPrefixByBlock} + + d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 12 { + return true + } + block := binary.BigEndian.Uint32(k[0:4]) + if block < startBlock || (endBlock > 0 && block > endBlock) { + return true + } + entryID := binary.BigEndian.Uint64(k[4:12]) + ids = append(ids, entryID) + return len(ids) < limit + }) + + return ids +} + +func (al *AuditLogger) queryByActor(d *dao.Simple, actor util.Uint160, startBlock, endBlock uint32, limit int) []uint64 { + var ids []uint64 + prefix := make([]byte, 21) + prefix[0] = auditPrefixByActor + copy(prefix[1:], actor.BytesBE()) + + d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 12 { + return true + } + block := binary.BigEndian.Uint32(k[0:4]) + if block < startBlock || (endBlock > 0 && block > endBlock) { + return true + } + entryID := binary.BigEndian.Uint64(k[4:12]) + ids = append(ids, entryID) + return len(ids) < limit + }) + + return ids +} + +func (al *AuditLogger) queryByTarget(d *dao.Simple, target util.Uint160, startBlock, endBlock uint32, limit int) []uint64 { + var ids []uint64 + prefix := make([]byte, 21) + prefix[0] = auditPrefixByTarget + copy(prefix[1:], target.BytesBE()) + + d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 12 { + return true + } + block := binary.BigEndian.Uint32(k[0:4]) + if block < startBlock || (endBlock > 0 && block > endBlock) { + return true + } + entryID := binary.BigEndian.Uint64(k[4:12]) + ids = append(ids, entryID) + return len(ids) < limit + }) + + return ids +} + +func (al *AuditLogger) queryByCategory(d *dao.Simple, category AuditCategory, startBlock, endBlock uint32, limit int) []uint64 { + var ids []uint64 + prefix := []byte{auditPrefixByCategory, byte(category)} + + d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 12 { + return true + } + block := binary.BigEndian.Uint32(k[0:4]) + if block < startBlock || (endBlock > 0 && block > endBlock) { + return true + } + entryID := binary.BigEndian.Uint64(k[4:12]) + ids = append(ids, entryID) + return len(ids) < limit + }) + + return ids +} + +func (al *AuditLogger) queryBySeverity(d *dao.Simple, minSeverity AuditSeverity, startBlock, endBlock uint32, limit int) []uint64 { + var ids []uint64 + + for sev := minSeverity; sev <= AuditSeverityCritical; sev++ { + prefix := []byte{auditPrefixBySeverity, byte(sev)} + + d.Seek(al.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 12 { + return true + } + block := binary.BigEndian.Uint32(k[0:4]) + if block < startBlock || (endBlock > 0 && block > endBlock) { + return true + } + entryID := binary.BigEndian.Uint64(k[4:12]) + ids = append(ids, entryID) + return len(ids) < limit + }) + + if len(ids) >= limit { + break + } + } + + return ids +} + +// Storage key helpers. +func (al *AuditLogger) makeEntryKey(entryID uint64) []byte { + key := make([]byte, 9) + key[0] = auditPrefixEntry + binary.BigEndian.PutUint64(key[1:], entryID) + return key +} + +func (al *AuditLogger) getNextEntryID(d *dao.Simple) uint64 { + key := []byte{auditPrefixCounter} + si := d.GetStorageItem(al.contractID, key) + + var nextID uint64 = 1 + if si != nil && len(si) >= 8 { + nextID = binary.BigEndian.Uint64(si) + 1 + } + + data := make([]byte, 8) + binary.BigEndian.PutUint64(data, nextID) + d.PutStorageItem(al.contractID, key, data) + + return nextID +} + +func (al *AuditLogger) putEntry(d *dao.Simple, entry *AuditEntry) { + key := al.makeEntryKey(entry.EntryID) + data := al.serializeEntry(entry) + d.PutStorageItem(al.contractID, key, data) +} + +func (al *AuditLogger) indexEntry(d *dao.Simple, entry *AuditEntry) { + // Index by block + blockKey := make([]byte, 13) + blockKey[0] = auditPrefixByBlock + binary.BigEndian.PutUint32(blockKey[1:5], entry.Timestamp) + binary.BigEndian.PutUint64(blockKey[5:13], entry.EntryID) + d.PutStorageItem(al.contractID, blockKey, []byte{1}) + + // Index by actor + if entry.Actor != (util.Uint160{}) { + actorKey := make([]byte, 33) + actorKey[0] = auditPrefixByActor + copy(actorKey[1:21], entry.Actor.BytesBE()) + binary.BigEndian.PutUint32(actorKey[21:25], entry.Timestamp) + binary.BigEndian.PutUint64(actorKey[25:33], entry.EntryID) + d.PutStorageItem(al.contractID, actorKey, []byte{1}) + } + + // Index by target + if entry.Target != (util.Uint160{}) { + targetKey := make([]byte, 33) + targetKey[0] = auditPrefixByTarget + copy(targetKey[1:21], entry.Target.BytesBE()) + binary.BigEndian.PutUint32(targetKey[21:25], entry.Timestamp) + binary.BigEndian.PutUint64(targetKey[25:33], entry.EntryID) + d.PutStorageItem(al.contractID, targetKey, []byte{1}) + } + + // Index by category + catKey := make([]byte, 14) + catKey[0] = auditPrefixByCategory + catKey[1] = byte(entry.Category) + binary.BigEndian.PutUint32(catKey[2:6], entry.Timestamp) + binary.BigEndian.PutUint64(catKey[6:14], entry.EntryID) + d.PutStorageItem(al.contractID, catKey, []byte{1}) + + // Index by severity + sevKey := make([]byte, 14) + sevKey[0] = auditPrefixBySeverity + sevKey[1] = byte(entry.Severity) + binary.BigEndian.PutUint32(sevKey[2:6], entry.Timestamp) + binary.BigEndian.PutUint64(sevKey[6:14], entry.EntryID) + d.PutStorageItem(al.contractID, sevKey, []byte{1}) + + // Index by contract + if entry.ContractID != 0 { + contractKey := make([]byte, 17) + contractKey[0] = auditPrefixByContract + binary.BigEndian.PutUint32(contractKey[1:5], uint32(entry.ContractID)) + binary.BigEndian.PutUint32(contractKey[5:9], entry.Timestamp) + binary.BigEndian.PutUint64(contractKey[9:17], entry.EntryID) + d.PutStorageItem(al.contractID, contractKey, []byte{1}) + } +} + +// Serialization helpers. +func (al *AuditLogger) serializeEntry(e *AuditEntry) []byte { + actionBytes := []byte(e.Action) + detailsBytes := []byte(e.Details) + + size := 8 + 4 + 1 + 1 + 1 + 4 + 20 + 20 + + 4 + len(actionBytes) + + 4 + len(e.ResourceID) + + 4 + len(detailsBytes) + + 32 + 32 + 32 + + data := make([]byte, size) + offset := 0 + + binary.BigEndian.PutUint64(data[offset:], e.EntryID) + offset += 8 + binary.BigEndian.PutUint32(data[offset:], e.Timestamp) + offset += 4 + data[offset] = byte(e.Category) + offset++ + data[offset] = byte(e.Severity) + offset++ + data[offset] = byte(e.Outcome) + offset++ + binary.BigEndian.PutUint32(data[offset:], uint32(e.ContractID)) + offset += 4 + copy(data[offset:], e.Actor.BytesBE()) + offset += 20 + copy(data[offset:], e.Target.BytesBE()) + offset += 20 + + binary.BigEndian.PutUint32(data[offset:], uint32(len(actionBytes))) + offset += 4 + copy(data[offset:], actionBytes) + offset += len(actionBytes) + + binary.BigEndian.PutUint32(data[offset:], uint32(len(e.ResourceID))) + offset += 4 + copy(data[offset:], e.ResourceID) + offset += len(e.ResourceID) + + binary.BigEndian.PutUint32(data[offset:], uint32(len(detailsBytes))) + offset += 4 + copy(data[offset:], detailsBytes) + offset += len(detailsBytes) + + copy(data[offset:], e.IPHash[:]) + offset += 32 + copy(data[offset:], e.PreviousState[:]) + offset += 32 + copy(data[offset:], e.NewState[:]) + + return data +} + +func (al *AuditLogger) deserializeEntry(data []byte) *AuditEntry { + if len(data) < 60 { + return nil + } + + e := &AuditEntry{} + offset := 0 + + e.EntryID = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + e.Timestamp = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + e.Category = AuditCategory(data[offset]) + offset++ + e.Severity = AuditSeverity(data[offset]) + offset++ + e.Outcome = AuditOutcome(data[offset]) + offset++ + e.ContractID = int32(binary.BigEndian.Uint32(data[offset:])) + offset += 4 + e.Actor, _ = util.Uint160DecodeBytesBE(data[offset : offset+20]) + offset += 20 + e.Target, _ = util.Uint160DecodeBytesBE(data[offset : offset+20]) + offset += 20 + + if offset+4 > len(data) { + return nil + } + actionLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(actionLen) > len(data) { + return nil + } + e.Action = string(data[offset : offset+int(actionLen)]) + offset += int(actionLen) + + if offset+4 > len(data) { + return nil + } + resourceLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(resourceLen) > len(data) { + return nil + } + e.ResourceID = make([]byte, resourceLen) + copy(e.ResourceID, data[offset:offset+int(resourceLen)]) + offset += int(resourceLen) + + if offset+4 > len(data) { + return nil + } + detailsLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(detailsLen) > len(data) { + return nil + } + e.Details = string(data[offset : offset+int(detailsLen)]) + offset += int(detailsLen) + + if offset+96 > len(data) { + return e // Return partial if hashes missing + } + copy(e.IPHash[:], data[offset:offset+32]) + offset += 32 + copy(e.PreviousState[:], data[offset:offset+32]) + offset += 32 + copy(e.NewState[:], data[offset:offset+32]) + + return e +} + +// StandardAuditActions defines common audit action names. +var StandardAuditActions = struct { + // Auth actions + AuthLogin string + AuthLogout string + AuthFailedLogin string + AuthRoleAssigned string + AuthRoleRevoked string + + // Access actions + AccessRead string + AccessWrite string + AccessDelete string + AccessDenied string + + // Financial actions + FinTransfer string + FinMint string + FinBurn string + FinInvest string + FinWithdraw string + + // Governance actions + GovProposalCreate string + GovVote string + GovProposalPass string + GovProposalReject string + + // Security actions + SecCircuitTrip string + SecRollback string + SecInvariantFail string + SecRightsRestrict string +}{ + AuthLogin: "auth.login", + AuthLogout: "auth.logout", + AuthFailedLogin: "auth.failed_login", + AuthRoleAssigned: "auth.role_assigned", + AuthRoleRevoked: "auth.role_revoked", + AccessRead: "access.read", + AccessWrite: "access.write", + AccessDelete: "access.delete", + AccessDenied: "access.denied", + FinTransfer: "financial.transfer", + FinMint: "financial.mint", + FinBurn: "financial.burn", + FinInvest: "financial.invest", + FinWithdraw: "financial.withdraw", + GovProposalCreate: "governance.proposal_create", + GovVote: "governance.vote", + GovProposalPass: "governance.proposal_pass", + GovProposalReject: "governance.proposal_reject", + SecCircuitTrip: "security.circuit_trip", + SecRollback: "security.rollback", + SecInvariantFail: "security.invariant_fail", + SecRightsRestrict: "security.rights_restrict", +} diff --git a/pkg/core/native/canary_deployment.go b/pkg/core/native/canary_deployment.go new file mode 100644 index 0000000..49021af --- /dev/null +++ b/pkg/core/native/canary_deployment.go @@ -0,0 +1,652 @@ +package native + +import ( + "encoding/binary" + "errors" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/core/storage" + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// ARCH-004: Canary Deployment System +// Provides infrastructure for safe, gradual rollouts of new features +// with automatic rollback capabilities based on error rates and invariant violations. + +// FeatureStatus represents the deployment status of a feature. +type FeatureStatus uint8 + +const ( + // FeatureDisabled means feature is not active + FeatureDisabled FeatureStatus = iota + // FeatureCanary means feature is active for a percentage of users + FeatureCanary + // FeatureRollingOut means feature is gradually expanding + FeatureRollingOut + // FeatureEnabled means feature is fully active + FeatureEnabled + // FeatureRolledBack means feature was active but reverted + FeatureRolledBack +) + +// RollbackReason identifies why a feature was rolled back. +type RollbackReason uint8 + +const ( + RollbackReasonManual RollbackReason = iota + RollbackReasonErrorRate + RollbackReasonInvariantViolation + RollbackReasonCircuitBreaker + RollbackReasonConsensusFailure + RollbackReasonPerformance +) + +// FeatureFlag represents a canary feature flag. +type FeatureFlag struct { + // Name is the unique identifier for this feature + Name string + // Description explains what this feature does + Description string + // Status is the current deployment status + Status FeatureStatus + // RolloutPercent is percentage of users who see this feature (0-100) + RolloutPercent uint8 + // TargetPercent is the goal percentage for gradual rollout + TargetPercent uint8 + // IncrementPercent is how much to increase per rollout step + IncrementPercent uint8 + // IncrementBlocks is blocks between rollout increments + IncrementBlocks uint32 + // StartBlock is when canary started + StartBlock uint32 + // LastIncrementBlock is when last rollout increment happened + LastIncrementBlock uint32 + // ErrorCount is errors encountered during canary + ErrorCount uint64 + // SuccessCount is successful operations during canary + SuccessCount uint64 + // MaxErrorRate is error rate that triggers rollback (per 10000) + MaxErrorRate uint32 + // MinSuccessCount is minimum successes before considering rollout + MinSuccessCount uint64 + // EnabledBy is who enabled the feature + EnabledBy util.Uint160 + // RollbackReason if rolled back + RollbackReason RollbackReason +} + +// FeatureMetrics tracks canary performance metrics. +type FeatureMetrics struct { + FeatureName string + TotalOperations uint64 + SuccessfulOps uint64 + FailedOps uint64 + AverageLatency uint64 // In block units + PeakLatency uint64 + InvariantChecks uint64 + InvariantFailures uint64 +} + +// Storage prefixes for canary deployment. +const ( + canaryPrefixFeature byte = 0xD0 // name -> FeatureFlag + canaryPrefixMetrics byte = 0xD1 // name -> FeatureMetrics + canaryPrefixHistory byte = 0xD2 // name + block -> status change + canaryPrefixUserEnabled byte = 0xD3 // name + address -> enabled flag + canaryPrefixGlobal byte = 0xD4 // -> GlobalCanaryState +) + +// Canary deployment errors. +var ( + ErrFeatureNotFound = errors.New("feature flag not found") + ErrFeatureAlreadyExists = errors.New("feature flag already exists") + ErrFeatureDisabled = errors.New("feature is disabled") + ErrRolloutInProgress = errors.New("rollout already in progress") + ErrInsufficientData = errors.New("insufficient data for rollout decision") + ErrErrorRateExceeded = errors.New("error rate exceeded threshold") +) + +// Default canary settings. +const ( + DefaultMaxErrorRate = 100 // 1% (per 10000) + DefaultMinSuccessCount = 100 + DefaultIncrementPercent = 10 + DefaultIncrementBlocks = 8640 // ~1 day at 10s blocks + DefaultCanaryPercent = 5 +) + +// CanaryDeployment manages feature flags and canary deployments. +type CanaryDeployment struct { + contractID int32 +} + +// NewCanaryDeployment creates a new canary deployment manager. +func NewCanaryDeployment(contractID int32) *CanaryDeployment { + return &CanaryDeployment{contractID: contractID} +} + +// CreateFeature creates a new feature flag. +func (cd *CanaryDeployment) CreateFeature(d *dao.Simple, name, description string, enabledBy util.Uint160) error { + if cd.GetFeature(d, name) != nil { + return ErrFeatureAlreadyExists + } + + feature := &FeatureFlag{ + Name: name, + Description: description, + Status: FeatureDisabled, + RolloutPercent: 0, + TargetPercent: 100, + IncrementPercent: DefaultIncrementPercent, + IncrementBlocks: DefaultIncrementBlocks, + MaxErrorRate: DefaultMaxErrorRate, + MinSuccessCount: DefaultMinSuccessCount, + EnabledBy: enabledBy, + } + + cd.putFeature(d, feature) + cd.initMetrics(d, name) + return nil +} + +// GetFeature retrieves a feature flag. +func (cd *CanaryDeployment) GetFeature(d *dao.Simple, name string) *FeatureFlag { + key := cd.makeFeatureKey(name) + si := d.GetStorageItem(cd.contractID, key) + if si == nil { + return nil + } + return cd.deserializeFeature(si) +} + +// StartCanary starts a canary deployment for a feature. +func (cd *CanaryDeployment) StartCanary(d *dao.Simple, name string, percent uint8, currentBlock uint32) error { + feature := cd.GetFeature(d, name) + if feature == nil { + return ErrFeatureNotFound + } + + if percent > 100 { + percent = 100 + } + + feature.Status = FeatureCanary + feature.RolloutPercent = percent + feature.StartBlock = currentBlock + feature.LastIncrementBlock = currentBlock + feature.ErrorCount = 0 + feature.SuccessCount = 0 + + cd.putFeature(d, feature) + cd.recordHistory(d, name, currentBlock, FeatureCanary) + return nil +} + +// StartRollout begins gradual rollout to target percentage. +func (cd *CanaryDeployment) StartRollout(d *dao.Simple, name string, targetPercent uint8, currentBlock uint32) error { + feature := cd.GetFeature(d, name) + if feature == nil { + return ErrFeatureNotFound + } + + if feature.Status == FeatureRollingOut { + return ErrRolloutInProgress + } + + // Require minimum success count before rollout + if feature.SuccessCount < feature.MinSuccessCount { + return ErrInsufficientData + } + + feature.Status = FeatureRollingOut + feature.TargetPercent = targetPercent + feature.LastIncrementBlock = currentBlock + + cd.putFeature(d, feature) + cd.recordHistory(d, name, currentBlock, FeatureRollingOut) + return nil +} + +// ProcessRolloutIncrement checks if rollout should increment. +func (cd *CanaryDeployment) ProcessRolloutIncrement(d *dao.Simple, name string, currentBlock uint32) { + feature := cd.GetFeature(d, name) + if feature == nil || feature.Status != FeatureRollingOut { + return + } + + // Check if enough blocks have passed + if currentBlock < feature.LastIncrementBlock+feature.IncrementBlocks { + return + } + + // Check error rate + if cd.shouldRollback(feature) { + cd.Rollback(d, name, RollbackReasonErrorRate, currentBlock) + return + } + + // Increment rollout + newPercent := feature.RolloutPercent + feature.IncrementPercent + if newPercent >= feature.TargetPercent { + newPercent = feature.TargetPercent + feature.Status = FeatureEnabled + } + + feature.RolloutPercent = newPercent + feature.LastIncrementBlock = currentBlock + cd.putFeature(d, feature) + + if feature.Status == FeatureEnabled { + cd.recordHistory(d, name, currentBlock, FeatureEnabled) + } +} + +// IsEnabled checks if a feature is enabled for a specific user. +func (cd *CanaryDeployment) IsEnabled(d *dao.Simple, name string, user util.Uint160) bool { + feature := cd.GetFeature(d, name) + if feature == nil { + return false + } + + switch feature.Status { + case FeatureEnabled: + return true + case FeatureDisabled, FeatureRolledBack: + return false + case FeatureCanary, FeatureRollingOut: + return cd.isInRolloutGroup(user, feature.RolloutPercent) + } + + return false +} + +// isInRolloutGroup determines if a user is in the rollout group. +func (cd *CanaryDeployment) isInRolloutGroup(user util.Uint160, percent uint8) bool { + if percent >= 100 { + return true + } + if percent == 0 { + return false + } + + // Use first byte of address as deterministic bucket + bucket := user[0] % 100 + return bucket < percent +} + +// RecordSuccess records a successful feature operation. +func (cd *CanaryDeployment) RecordSuccess(d *dao.Simple, name string) { + feature := cd.GetFeature(d, name) + if feature == nil { + return + } + + feature.SuccessCount++ + cd.putFeature(d, feature) + + metrics := cd.GetMetrics(d, name) + if metrics != nil { + metrics.TotalOperations++ + metrics.SuccessfulOps++ + cd.putMetrics(d, metrics) + } +} + +// RecordError records a feature operation error. +func (cd *CanaryDeployment) RecordError(d *dao.Simple, name string, currentBlock uint32) { + feature := cd.GetFeature(d, name) + if feature == nil { + return + } + + feature.ErrorCount++ + cd.putFeature(d, feature) + + metrics := cd.GetMetrics(d, name) + if metrics != nil { + metrics.TotalOperations++ + metrics.FailedOps++ + cd.putMetrics(d, metrics) + } + + // Check if should auto-rollback + if cd.shouldRollback(feature) { + cd.Rollback(d, name, RollbackReasonErrorRate, currentBlock) + } +} + +// shouldRollback checks if error rate exceeds threshold. +func (cd *CanaryDeployment) shouldRollback(feature *FeatureFlag) bool { + if feature.SuccessCount+feature.ErrorCount < 10 { + return false // Not enough data + } + + errorRate := (feature.ErrorCount * 10000) / (feature.SuccessCount + feature.ErrorCount) + return uint32(errorRate) > feature.MaxErrorRate +} + +// Rollback rolls back a feature to disabled state. +func (cd *CanaryDeployment) Rollback(d *dao.Simple, name string, reason RollbackReason, currentBlock uint32) error { + feature := cd.GetFeature(d, name) + if feature == nil { + return ErrFeatureNotFound + } + + feature.Status = FeatureRolledBack + feature.RolloutPercent = 0 + feature.RollbackReason = reason + + cd.putFeature(d, feature) + cd.recordHistory(d, name, currentBlock, FeatureRolledBack) + return nil +} + +// Enable fully enables a feature (100% rollout). +func (cd *CanaryDeployment) Enable(d *dao.Simple, name string, currentBlock uint32) error { + feature := cd.GetFeature(d, name) + if feature == nil { + return ErrFeatureNotFound + } + + feature.Status = FeatureEnabled + feature.RolloutPercent = 100 + + cd.putFeature(d, feature) + cd.recordHistory(d, name, currentBlock, FeatureEnabled) + return nil +} + +// Disable disables a feature. +func (cd *CanaryDeployment) Disable(d *dao.Simple, name string, currentBlock uint32) error { + feature := cd.GetFeature(d, name) + if feature == nil { + return ErrFeatureNotFound + } + + feature.Status = FeatureDisabled + feature.RolloutPercent = 0 + + cd.putFeature(d, feature) + cd.recordHistory(d, name, currentBlock, FeatureDisabled) + return nil +} + +// GetMetrics retrieves feature metrics. +func (cd *CanaryDeployment) GetMetrics(d *dao.Simple, name string) *FeatureMetrics { + key := cd.makeMetricsKey(name) + si := d.GetStorageItem(cd.contractID, key) + if si == nil { + return nil + } + return cd.deserializeMetrics(si) +} + +// GetAllFeatures retrieves all feature flags. +func (cd *CanaryDeployment) GetAllFeatures(d *dao.Simple) []*FeatureFlag { + var features []*FeatureFlag + prefix := []byte{canaryPrefixFeature} + + d.Seek(cd.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if feature := cd.deserializeFeature(v); feature != nil { + features = append(features, feature) + } + return true + }) + + return features +} + +// GetActiveCanaries retrieves features currently in canary/rollout. +func (cd *CanaryDeployment) GetActiveCanaries(d *dao.Simple) []*FeatureFlag { + var features []*FeatureFlag + prefix := []byte{canaryPrefixFeature} + + d.Seek(cd.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if feature := cd.deserializeFeature(v); feature != nil { + if feature.Status == FeatureCanary || feature.Status == FeatureRollingOut { + features = append(features, feature) + } + } + return true + }) + + return features +} + +// Helper methods. +func (cd *CanaryDeployment) makeFeatureKey(name string) []byte { + key := make([]byte, 1+len(name)) + key[0] = canaryPrefixFeature + copy(key[1:], name) + return key +} + +func (cd *CanaryDeployment) makeMetricsKey(name string) []byte { + key := make([]byte, 1+len(name)) + key[0] = canaryPrefixMetrics + copy(key[1:], name) + return key +} + +func (cd *CanaryDeployment) putFeature(d *dao.Simple, feature *FeatureFlag) { + key := cd.makeFeatureKey(feature.Name) + data := cd.serializeFeature(feature) + d.PutStorageItem(cd.contractID, key, data) +} + +func (cd *CanaryDeployment) initMetrics(d *dao.Simple, name string) { + metrics := &FeatureMetrics{FeatureName: name} + cd.putMetrics(d, metrics) +} + +func (cd *CanaryDeployment) putMetrics(d *dao.Simple, metrics *FeatureMetrics) { + key := cd.makeMetricsKey(metrics.FeatureName) + data := cd.serializeMetrics(metrics) + d.PutStorageItem(cd.contractID, key, data) +} + +func (cd *CanaryDeployment) recordHistory(d *dao.Simple, name string, blockHeight uint32, status FeatureStatus) { + key := make([]byte, 1+len(name)+4) + key[0] = canaryPrefixHistory + copy(key[1:], name) + binary.BigEndian.PutUint32(key[1+len(name):], blockHeight) + + data := []byte{byte(status)} + d.PutStorageItem(cd.contractID, key, data) +} + +// Serialization helpers. +func (cd *CanaryDeployment) serializeFeature(f *FeatureFlag) []byte { + nameBytes := []byte(f.Name) + descBytes := []byte(f.Description) + + size := 4 + len(nameBytes) + 4 + len(descBytes) + 1 + 4 + 4 + 4 + 4 + 4 + 8 + 8 + 4 + 8 + 20 + 1 + data := make([]byte, size) + offset := 0 + + binary.BigEndian.PutUint32(data[offset:], uint32(len(nameBytes))) + offset += 4 + copy(data[offset:], nameBytes) + offset += len(nameBytes) + + binary.BigEndian.PutUint32(data[offset:], uint32(len(descBytes))) + offset += 4 + copy(data[offset:], descBytes) + offset += len(descBytes) + + data[offset] = byte(f.Status) + offset++ + + data[offset] = f.RolloutPercent + offset++ + data[offset] = f.TargetPercent + offset++ + data[offset] = f.IncrementPercent + offset++ + + binary.BigEndian.PutUint32(data[offset:], f.IncrementBlocks) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], f.StartBlock) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], f.LastIncrementBlock) + offset += 4 + + binary.BigEndian.PutUint64(data[offset:], f.ErrorCount) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], f.SuccessCount) + offset += 8 + + binary.BigEndian.PutUint32(data[offset:], f.MaxErrorRate) + offset += 4 + binary.BigEndian.PutUint64(data[offset:], f.MinSuccessCount) + offset += 8 + + copy(data[offset:], f.EnabledBy.BytesBE()) + offset += 20 + + data[offset] = byte(f.RollbackReason) + + return data +} + +func (cd *CanaryDeployment) deserializeFeature(data []byte) *FeatureFlag { + if len(data) < 8 { + return nil + } + + f := &FeatureFlag{} + offset := 0 + + nameLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(nameLen) > len(data) { + return nil + } + f.Name = string(data[offset : offset+int(nameLen)]) + offset += int(nameLen) + + if offset+4 > len(data) { + return nil + } + descLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(descLen) > len(data) { + return nil + } + f.Description = string(data[offset : offset+int(descLen)]) + offset += int(descLen) + + if offset+61 > len(data) { + return nil + } + + f.Status = FeatureStatus(data[offset]) + offset++ + f.RolloutPercent = data[offset] + offset++ + f.TargetPercent = data[offset] + offset++ + f.IncrementPercent = data[offset] + offset++ + + f.IncrementBlocks = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + f.StartBlock = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + f.LastIncrementBlock = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + + f.ErrorCount = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + f.SuccessCount = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + + f.MaxErrorRate = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + f.MinSuccessCount = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + + f.EnabledBy, _ = util.Uint160DecodeBytesBE(data[offset : offset+20]) + offset += 20 + + f.RollbackReason = RollbackReason(data[offset]) + + return f +} + +func (cd *CanaryDeployment) serializeMetrics(m *FeatureMetrics) []byte { + nameBytes := []byte(m.FeatureName) + data := make([]byte, 4+len(nameBytes)+48) + offset := 0 + + binary.BigEndian.PutUint32(data[offset:], uint32(len(nameBytes))) + offset += 4 + copy(data[offset:], nameBytes) + offset += len(nameBytes) + + binary.BigEndian.PutUint64(data[offset:], m.TotalOperations) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], m.SuccessfulOps) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], m.FailedOps) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], m.AverageLatency) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], m.PeakLatency) + offset += 8 + binary.BigEndian.PutUint64(data[offset:], m.InvariantChecks) + + return data +} + +func (cd *CanaryDeployment) deserializeMetrics(data []byte) *FeatureMetrics { + if len(data) < 8 { + return nil + } + + m := &FeatureMetrics{} + offset := 0 + + nameLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(nameLen) > len(data) { + return nil + } + m.FeatureName = string(data[offset : offset+int(nameLen)]) + offset += int(nameLen) + + if offset+48 > len(data) { + return nil + } + + m.TotalOperations = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + m.SuccessfulOps = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + m.FailedOps = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + m.AverageLatency = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + m.PeakLatency = binary.BigEndian.Uint64(data[offset:]) + offset += 8 + m.InvariantChecks = binary.BigEndian.Uint64(data[offset:]) + + return m +} + +// StandardFeatureFlags defines common feature flag names. +var StandardFeatureFlags = struct { + VitaRecoveryV2 string + TributeGamingDetect string + CrossChainProofs string + CommitRevealInvest string + EnhancedAuditLogging string + CircuitBreakerAuto string +}{ + VitaRecoveryV2: "vita_recovery_v2", + TributeGamingDetect: "tribute_gaming_detect", + CrossChainProofs: "cross_chain_proofs", + CommitRevealInvest: "commit_reveal_invest", + EnhancedAuditLogging: "enhanced_audit_logging", + CircuitBreakerAuto: "circuit_breaker_auto", +} diff --git a/pkg/core/native/circuit_breaker.go b/pkg/core/native/circuit_breaker.go new file mode 100644 index 0000000..a51dccb --- /dev/null +++ b/pkg/core/native/circuit_breaker.go @@ -0,0 +1,555 @@ +package native + +import ( + "encoding/binary" + "errors" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/core/storage" + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// ARCH-001: Circuit Breaker System +// Provides automatic protection against anomalous behavior by halting +// contract operations when thresholds are exceeded. This is a critical +// safety mechanism for production deployments. + +// CircuitState represents the current state of a circuit breaker. +type CircuitState uint8 + +const ( + // CircuitClosed means normal operation (requests flow through) + CircuitClosed CircuitState = iota + // CircuitOpen means halted (requests are rejected) + CircuitOpen + // CircuitHalfOpen means testing recovery (limited requests allowed) + CircuitHalfOpen +) + +// TripReason identifies why a circuit breaker was tripped. +type TripReason uint8 + +const ( + TripReasonManual TripReason = iota + TripReasonRateLimit + TripReasonBalanceAnomaly + TripReasonSecurityBreach + TripReasonExternalDependency + TripReasonResourceExhaustion + TripReasonConsensusFailure +) + +// CircuitBreakerConfig contains settings for a circuit breaker. +type CircuitBreakerConfig struct { + // Name identifies this circuit breaker + Name string + // ContractID is the contract this breaker protects + ContractID int32 + // FailureThreshold is failures before tripping + FailureThreshold uint32 + // SuccessThreshold is successes needed to close after half-open + SuccessThreshold uint32 + // TimeoutBlocks is blocks before moving from open to half-open + TimeoutBlocks uint32 + // CooldownBlocks is minimum blocks between state changes + CooldownBlocks uint32 + // AutoRecover determines if breaker can auto-recover + AutoRecover bool +} + +// CircuitBreakerState tracks the current state of a circuit breaker. +type CircuitBreakerState struct { + State CircuitState + FailureCount uint32 + SuccessCount uint32 + LastStateChange uint32 + LastFailure uint32 + TripReason TripReason + TrippedBy util.Uint160 + TotalTrips uint64 + ConsecutiveTrips uint32 +} + +// Storage prefixes for circuit breakers. +const ( + circuitPrefixConfig byte = 0xCB // name -> CircuitBreakerConfig + circuitPrefixState byte = 0xCC // name -> CircuitBreakerState + circuitPrefixHistory byte = 0xCD // name + timestamp -> TripEvent + circuitPrefixGlobal byte = 0xCE // -> GlobalCircuitState +) + +// Circuit breaker errors. +var ( + ErrCircuitOpen = errors.New("circuit breaker is open") + ErrCircuitHalfOpen = errors.New("circuit breaker is half-open, limited operations") + ErrCircuitCooldown = errors.New("circuit breaker cooldown period active") + ErrCircuitNotFound = errors.New("circuit breaker not found") + ErrCircuitAutoRecover = errors.New("circuit breaker cannot be manually closed when auto-recover enabled") +) + +// Default circuit breaker settings. +const ( + DefaultFailureThreshold = 10 + DefaultSuccessThreshold = 5 + DefaultTimeoutBlocks = 100 + DefaultCooldownBlocks = 10 +) + +// CircuitBreaker provides circuit breaker functionality for contracts. +type CircuitBreaker struct { + contractID int32 +} + +// NewCircuitBreaker creates a new circuit breaker manager. +func NewCircuitBreaker(contractID int32) *CircuitBreaker { + return &CircuitBreaker{contractID: contractID} +} + +// RegisterBreaker registers a new circuit breaker with configuration. +func (cb *CircuitBreaker) RegisterBreaker(d *dao.Simple, cfg *CircuitBreakerConfig) { + key := cb.makeConfigKey(cfg.Name) + data := cb.serializeConfig(cfg) + d.PutStorageItem(cb.contractID, key, data) + + // Initialize state as closed + state := &CircuitBreakerState{ + State: CircuitClosed, + } + cb.putState(d, cfg.Name, state) +} + +// GetState retrieves the current state of a circuit breaker. +func (cb *CircuitBreaker) GetState(d *dao.Simple, name string) *CircuitBreakerState { + key := cb.makeStateKey(name) + si := d.GetStorageItem(cb.contractID, key) + if si == nil { + return nil + } + return cb.deserializeState(si) +} + +// GetConfig retrieves circuit breaker configuration. +func (cb *CircuitBreaker) GetConfig(d *dao.Simple, name string) *CircuitBreakerConfig { + key := cb.makeConfigKey(name) + si := d.GetStorageItem(cb.contractID, key) + if si == nil { + return nil + } + return cb.deserializeConfig(si) +} + +// AllowRequest checks if a request should be allowed through. +func (cb *CircuitBreaker) AllowRequest(d *dao.Simple, name string, currentBlock uint32) error { + state := cb.GetState(d, name) + if state == nil { + return ErrCircuitNotFound + } + + switch state.State { + case CircuitClosed: + return nil + case CircuitOpen: + cfg := cb.GetConfig(d, name) + if cfg == nil { + return ErrCircuitNotFound + } + // Check if timeout has elapsed for potential recovery + if cfg.AutoRecover && currentBlock >= state.LastStateChange+cfg.TimeoutBlocks { + // Transition to half-open + cb.transitionState(d, name, CircuitHalfOpen, currentBlock) + return ErrCircuitHalfOpen + } + return ErrCircuitOpen + case CircuitHalfOpen: + return ErrCircuitHalfOpen + } + + return nil +} + +// RecordSuccess records a successful operation. +func (cb *CircuitBreaker) RecordSuccess(d *dao.Simple, name string, currentBlock uint32) { + state := cb.GetState(d, name) + if state == nil { + return + } + + if state.State == CircuitHalfOpen { + state.SuccessCount++ + cfg := cb.GetConfig(d, name) + if cfg != nil && state.SuccessCount >= cfg.SuccessThreshold { + // Close the circuit + cb.transitionState(d, name, CircuitClosed, currentBlock) + return + } + } + + // Reset failure count on success when closed + if state.State == CircuitClosed { + state.FailureCount = 0 + } + cb.putState(d, name, state) +} + +// RecordFailure records a failed operation. +func (cb *CircuitBreaker) RecordFailure(d *dao.Simple, name string, currentBlock uint32, reason TripReason) { + state := cb.GetState(d, name) + if state == nil { + return + } + + state.FailureCount++ + state.LastFailure = currentBlock + + cfg := cb.GetConfig(d, name) + if cfg == nil { + return + } + + switch state.State { + case CircuitClosed: + if state.FailureCount >= cfg.FailureThreshold { + cb.tripBreaker(d, name, currentBlock, reason, util.Uint160{}) + } else { + cb.putState(d, name, state) + } + case CircuitHalfOpen: + // Any failure in half-open immediately trips + cb.tripBreaker(d, name, currentBlock, reason, util.Uint160{}) + } +} + +// TripBreaker manually trips a circuit breaker. +func (cb *CircuitBreaker) TripBreaker(d *dao.Simple, name string, currentBlock uint32, reason TripReason, tripper util.Uint160) error { + state := cb.GetState(d, name) + if state == nil { + return ErrCircuitNotFound + } + + cfg := cb.GetConfig(d, name) + if cfg == nil { + return ErrCircuitNotFound + } + + // Check cooldown + if currentBlock < state.LastStateChange+cfg.CooldownBlocks { + return ErrCircuitCooldown + } + + cb.tripBreaker(d, name, currentBlock, reason, tripper) + return nil +} + +// ResetBreaker manually resets a circuit breaker to closed. +func (cb *CircuitBreaker) ResetBreaker(d *dao.Simple, name string, currentBlock uint32) error { + state := cb.GetState(d, name) + if state == nil { + return ErrCircuitNotFound + } + + cfg := cb.GetConfig(d, name) + if cfg == nil { + return ErrCircuitNotFound + } + + // Check cooldown + if currentBlock < state.LastStateChange+cfg.CooldownBlocks { + return ErrCircuitCooldown + } + + cb.transitionState(d, name, CircuitClosed, currentBlock) + return nil +} + +// tripBreaker internal method to trip the breaker. +func (cb *CircuitBreaker) tripBreaker(d *dao.Simple, name string, currentBlock uint32, reason TripReason, tripper util.Uint160) { + state := cb.GetState(d, name) + if state == nil { + return + } + + state.State = CircuitOpen + state.TripReason = reason + state.TrippedBy = tripper + state.LastStateChange = currentBlock + state.TotalTrips++ + state.ConsecutiveTrips++ + state.SuccessCount = 0 + + cb.putState(d, name, state) + cb.recordTripEvent(d, name, currentBlock, reason, tripper) +} + +// transitionState changes the circuit state. +func (cb *CircuitBreaker) transitionState(d *dao.Simple, name string, newState CircuitState, currentBlock uint32) { + state := cb.GetState(d, name) + if state == nil { + return + } + + state.State = newState + state.LastStateChange = currentBlock + + if newState == CircuitClosed { + state.FailureCount = 0 + state.SuccessCount = 0 + state.ConsecutiveTrips = 0 + } else if newState == CircuitHalfOpen { + state.SuccessCount = 0 + } + + cb.putState(d, name, state) +} + +// TripEvent records a circuit breaker trip for auditing. +type TripEvent struct { + Name string + BlockHeight uint32 + Reason TripReason + TrippedBy util.Uint160 +} + +// recordTripEvent stores a trip event in history. +func (cb *CircuitBreaker) recordTripEvent(d *dao.Simple, name string, blockHeight uint32, reason TripReason, tripper util.Uint160) { + key := make([]byte, 1+len(name)+4) + key[0] = circuitPrefixHistory + copy(key[1:], name) + binary.BigEndian.PutUint32(key[1+len(name):], blockHeight) + + data := make([]byte, 21) + data[0] = byte(reason) + copy(data[1:], tripper.BytesBE()) + d.PutStorageItem(cb.contractID, key, data) +} + +// GetTripHistory retrieves trip history for a circuit breaker. +func (cb *CircuitBreaker) GetTripHistory(d *dao.Simple, name string, limit int) []TripEvent { + var events []TripEvent + prefix := make([]byte, 1+len(name)) + prefix[0] = circuitPrefixHistory + copy(prefix[1:], name) + + count := 0 + d.Seek(cb.contractID, storage.SeekRange{Prefix: prefix, Backwards: true}, func(k, v []byte) bool { + if count >= limit || len(k) < 4 || len(v) < 21 { + return false + } + event := TripEvent{ + Name: name, + BlockHeight: binary.BigEndian.Uint32(k[len(k)-4:]), + Reason: TripReason(v[0]), + } + event.TrippedBy, _ = util.Uint160DecodeBytesBE(v[1:21]) + events = append(events, event) + count++ + return true + }) + + return events +} + +// IsOpen returns true if the circuit is open (blocking requests). +func (cb *CircuitBreaker) IsOpen(d *dao.Simple, name string) bool { + state := cb.GetState(d, name) + return state != nil && state.State == CircuitOpen +} + +// IsClosed returns true if the circuit is closed (allowing requests). +func (cb *CircuitBreaker) IsClosed(d *dao.Simple, name string) bool { + state := cb.GetState(d, name) + return state != nil && state.State == CircuitClosed +} + +// GlobalCircuitState tracks system-wide circuit breaker status. +type GlobalCircuitState struct { + // EmergencyShutdown halts all protected operations + EmergencyShutdown bool + // ShutdownBlock is when emergency was triggered + ShutdownBlock uint32 + // ShutdownBy is who triggered emergency + ShutdownBy util.Uint160 + // ActiveBreakers is count of currently open breakers + ActiveBreakers uint32 +} + +// GetGlobalState retrieves the global circuit breaker state. +func (cb *CircuitBreaker) GetGlobalState(d *dao.Simple) *GlobalCircuitState { + key := []byte{circuitPrefixGlobal} + si := d.GetStorageItem(cb.contractID, key) + if si == nil { + return &GlobalCircuitState{} + } + if len(si) < 26 { + return &GlobalCircuitState{} + } + + return &GlobalCircuitState{ + EmergencyShutdown: si[0] == 1, + ShutdownBlock: binary.BigEndian.Uint32(si[1:5]), + ShutdownBy: mustDecodeUint160(si[5:25]), + ActiveBreakers: binary.BigEndian.Uint32(si[25:29]), + } +} + +// SetEmergencyShutdown enables or disables emergency shutdown. +func (cb *CircuitBreaker) SetEmergencyShutdown(d *dao.Simple, enabled bool, blockHeight uint32, triggeredBy util.Uint160) { + state := cb.GetGlobalState(d) + state.EmergencyShutdown = enabled + if enabled { + state.ShutdownBlock = blockHeight + state.ShutdownBy = triggeredBy + } + + key := []byte{circuitPrefixGlobal} + data := make([]byte, 29) + if state.EmergencyShutdown { + data[0] = 1 + } + binary.BigEndian.PutUint32(data[1:5], state.ShutdownBlock) + copy(data[5:25], state.ShutdownBy.BytesBE()) + binary.BigEndian.PutUint32(data[25:29], state.ActiveBreakers) + d.PutStorageItem(cb.contractID, key, data) +} + +// IsEmergencyShutdown returns true if emergency shutdown is active. +func (cb *CircuitBreaker) IsEmergencyShutdown(d *dao.Simple) bool { + state := cb.GetGlobalState(d) + return state.EmergencyShutdown +} + +// Helper methods. +func (cb *CircuitBreaker) makeConfigKey(name string) []byte { + key := make([]byte, 1+len(name)) + key[0] = circuitPrefixConfig + copy(key[1:], name) + return key +} + +func (cb *CircuitBreaker) makeStateKey(name string) []byte { + key := make([]byte, 1+len(name)) + key[0] = circuitPrefixState + copy(key[1:], name) + return key +} + +func (cb *CircuitBreaker) putState(d *dao.Simple, name string, state *CircuitBreakerState) { + key := cb.makeStateKey(name) + data := cb.serializeState(state) + d.PutStorageItem(cb.contractID, key, data) +} + +// Serialization helpers. +func (cb *CircuitBreaker) serializeConfig(cfg *CircuitBreakerConfig) []byte { + nameBytes := []byte(cfg.Name) + data := make([]byte, 4+len(nameBytes)+4+16+1) + offset := 0 + + binary.BigEndian.PutUint32(data[offset:], uint32(len(nameBytes))) + offset += 4 + copy(data[offset:], nameBytes) + offset += len(nameBytes) + + binary.BigEndian.PutUint32(data[offset:], uint32(cfg.ContractID)) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], cfg.FailureThreshold) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], cfg.SuccessThreshold) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], cfg.TimeoutBlocks) + offset += 4 + binary.BigEndian.PutUint32(data[offset:], cfg.CooldownBlocks) + offset += 4 + if cfg.AutoRecover { + data[offset] = 1 + } + + return data +} + +func (cb *CircuitBreaker) deserializeConfig(data []byte) *CircuitBreakerConfig { + if len(data) < 8 { + return nil + } + cfg := &CircuitBreakerConfig{} + offset := 0 + + nameLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(nameLen) > len(data) { + return nil + } + cfg.Name = string(data[offset : offset+int(nameLen)]) + offset += int(nameLen) + + if offset+17 > len(data) { + return nil + } + + cfg.ContractID = int32(binary.BigEndian.Uint32(data[offset:])) + offset += 4 + cfg.FailureThreshold = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + cfg.SuccessThreshold = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + cfg.TimeoutBlocks = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + cfg.CooldownBlocks = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + cfg.AutoRecover = data[offset] == 1 + + return cfg +} + +func (cb *CircuitBreaker) serializeState(state *CircuitBreakerState) []byte { + data := make([]byte, 46) + data[0] = byte(state.State) + binary.BigEndian.PutUint32(data[1:5], state.FailureCount) + binary.BigEndian.PutUint32(data[5:9], state.SuccessCount) + binary.BigEndian.PutUint32(data[9:13], state.LastStateChange) + binary.BigEndian.PutUint32(data[13:17], state.LastFailure) + data[17] = byte(state.TripReason) + copy(data[18:38], state.TrippedBy.BytesBE()) + binary.BigEndian.PutUint64(data[38:46], state.TotalTrips) + return data +} + +func (cb *CircuitBreaker) deserializeState(data []byte) *CircuitBreakerState { + if len(data) < 46 { + return nil + } + state := &CircuitBreakerState{ + State: CircuitState(data[0]), + FailureCount: binary.BigEndian.Uint32(data[1:5]), + SuccessCount: binary.BigEndian.Uint32(data[5:9]), + LastStateChange: binary.BigEndian.Uint32(data[9:13]), + LastFailure: binary.BigEndian.Uint32(data[13:17]), + TripReason: TripReason(data[17]), + TotalTrips: binary.BigEndian.Uint64(data[38:46]), + } + state.TrippedBy, _ = util.Uint160DecodeBytesBE(data[18:38]) + return state +} + +func mustDecodeUint160(data []byte) util.Uint160 { + u, _ := util.Uint160DecodeBytesBE(data) + return u +} + +// StandardCircuitBreakers defines common circuit breaker names. +var StandardCircuitBreakers = struct { + VTSTransfers string + VitaRegistration string + CrossChainBridge string + HealthRecords string + InvestmentOps string + GovernanceVoting string + TributeAssessment string +}{ + VTSTransfers: "vts_transfers", + VitaRegistration: "vita_registration", + CrossChainBridge: "cross_chain_bridge", + HealthRecords: "health_records", + InvestmentOps: "investment_ops", + GovernanceVoting: "governance_voting", + TributeAssessment: "tribute_assessment", +} diff --git a/pkg/core/native/collocatio.go b/pkg/core/native/collocatio.go old mode 100644 new mode 100755 index 42cf3b7..a8dad4c --- a/pkg/core/native/collocatio.go +++ b/pkg/core/native/collocatio.go @@ -1,2178 +1,2178 @@ -package native - -import ( - "encoding/binary" - "fmt" - "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/nativehashes" - "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/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" -) - -// Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO). -// Latin: "collocatio" = placement, arrangement (investment) -type Collocatio struct { - interop.ContractMD - Tutus ITutus - Vita IVita - RoleRegistry *RoleRegistry - VTS *VTS - Scire *Scire - Eligere *Eligere - Tribute *Tribute -} - -// Storage prefixes for Collocatio contract. -const ( - collocatioPrefixConfig byte = 0x01 // -> CollocatioConfig - collocatioPrefixOpportunity byte = 0x10 // opportunityID -> InvestmentOpportunity - collocatioPrefixOpportunityByType byte = 0x11 // type + opportunityID -> exists - collocatioPrefixOpportunityByStatus byte = 0x12 // status + opportunityID -> exists - collocatioPrefixOppCounter byte = 0x1F // -> next opportunity ID - collocatioPrefixInvestment byte = 0x20 // investmentID -> Investment - collocatioPrefixInvestmentByOpp byte = 0x21 // opportunityID + investmentID -> exists - collocatioPrefixInvestmentByInvestor byte = 0x22 // vitaID + investmentID -> exists - collocatioPrefixInvCounter byte = 0x2F // -> next investment ID - collocatioPrefixEligibility byte = 0x30 // vitaID -> InvestorEligibility - collocatioPrefixEligibilityByOwner byte = 0x31 // owner -> vitaID - collocatioPrefixViolation byte = 0x40 // violationID -> InvestmentViolation - collocatioPrefixViolationByInvestor byte = 0x41 // vitaID + violationID -> exists - collocatioPrefixViolationCounter byte = 0x4F // -> next violation ID - collocatioPrefixEmployment byte = 0x50 // employeeVitaID -> EmploymentVerification - collocatioPrefixEmploymentByEmployer byte = 0x51 // employerVitaID + employeeVitaID -> exists - collocatioPrefixContractor byte = 0x60 // contractorVitaID -> ContractorVerification - collocatioPrefixContractorByPlatform byte = 0x61 // platformHash + contractorVitaID -> exists -) - -// Collocatio events. -const ( - collocatioOpportunityCreatedEvent = "OpportunityCreated" - collocatioOpportunityActivatedEvent = "OpportunityActivated" - collocatioOpportunityClosedEvent = "OpportunityClosed" - collocatioOpportunityFailedEvent = "OpportunityFailed" - collocatioOpportunityCancelledEvent = "OpportunityCancelled" - collocatioInvestmentMadeEvent = "InvestmentMade" - collocatioInvestmentWithdrawnEvent = "InvestmentWithdrawn" - collocatioReturnsDistributedEvent = "ReturnsDistributed" - collocatioEligibilityUpdatedEvent = "EligibilityUpdated" - collocatioEducationCompletedEvent = "EducationCompleted" - collocatioViolationRecordedEvent = "ViolationRecorded" - collocatioViolationResolvedEvent = "ViolationResolved" - collocatioEmploymentVerifiedEvent = "EmploymentVerified" - collocatioEmploymentRevokedEvent = "EmploymentRevoked" - collocatioContractorVerifiedEvent = "ContractorVerified" - collocatioContractorRevokedEvent = "ContractorRevoked" -) - -// RoleInvestmentManager is the role ID for investment management. -const RoleInvestmentManager uint64 = 28 - -// Default config values. -const ( - defaultMinPIOParticipants uint64 = 100 - defaultMinEIOParticipants uint64 = 10 - defaultMinCIOParticipants uint64 = 25 - defaultMinInvestment uint64 = 100_00000000 // 100 VTS - defaultMaxIndividualCap uint64 = 1_000_000_00000000 // 1M VTS - defaultWealthConcentration uint64 = 500 // 5% - defaultCreationFee uint64 = 1000_00000000 // 1000 VTS - defaultInvestmentFee uint64 = 50 // 0.5% - defaultWithdrawalPenalty uint64 = 200 // 2% - defaultMinVotingPeriod uint32 = 10000 - defaultMinInvestmentPeriod uint32 = 20000 - defaultMinMaturityPeriod uint32 = 50000 - defaultMaxViolationsBeforeBan uint8 = 3 -) - -var _ interop.Contract = (*Collocatio)(nil) - -func newCollocatio() *Collocatio { - c := &Collocatio{ - ContractMD: *interop.NewContractMD(nativenames.Collocatio, nativeids.Collocatio), - } - defer c.BuildHFSpecificMD(c.ActiveIn()) - - // getConfig - desc := NewDescriptor("getConfig", smartcontract.ArrayType) - md := NewMethodAndPrice(c.getConfig, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // getOpportunityCount - desc = NewDescriptor("getOpportunityCount", smartcontract.IntegerType) - md = NewMethodAndPrice(c.getOpportunityCount, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // getOpportunity - desc = NewDescriptor("getOpportunity", smartcontract.ArrayType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getOpportunity, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // createOpportunity - desc = NewDescriptor("createOpportunity", smartcontract.IntegerType, - manifest.NewParameter("oppType", smartcontract.IntegerType), - manifest.NewParameter("name", smartcontract.StringType), - manifest.NewParameter("description", smartcontract.StringType), - manifest.NewParameter("termsHash", smartcontract.Hash256Type), - manifest.NewParameter("minParticipants", smartcontract.IntegerType), - manifest.NewParameter("maxParticipants", smartcontract.IntegerType), - manifest.NewParameter("minInvestment", smartcontract.IntegerType), - manifest.NewParameter("maxInvestment", smartcontract.IntegerType), - manifest.NewParameter("targetPool", smartcontract.IntegerType), - manifest.NewParameter("expectedReturns", smartcontract.IntegerType), - manifest.NewParameter("riskLevel", smartcontract.IntegerType), - manifest.NewParameter("maturityBlocks", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.createOpportunity, 1<<17, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // activateOpportunity - desc = NewDescriptor("activateOpportunity", smartcontract.BoolType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.activateOpportunity, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // invest - desc = NewDescriptor("invest", smartcontract.IntegerType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.invest, 1<<17, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // withdraw - desc = NewDescriptor("withdraw", smartcontract.BoolType, - manifest.NewParameter("investmentID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.withdraw, 1<<17, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // getEligibility - desc = NewDescriptor("getEligibility", smartcontract.ArrayType, - manifest.NewParameter("investor", smartcontract.Hash160Type)) - md = NewMethodAndPrice(c.getEligibility, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // setEligibility - desc = NewDescriptor("setEligibility", smartcontract.BoolType, - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("eligibilityFlags", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.setEligibility, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // isEligible - desc = NewDescriptor("isEligible", smartcontract.BoolType, - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("oppType", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.isEligible, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // getInvestment - desc = NewDescriptor("getInvestment", smartcontract.ArrayType, - manifest.NewParameter("investmentID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getInvestment, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // getInvestmentCount - desc = NewDescriptor("getInvestmentCount", smartcontract.IntegerType) - md = NewMethodAndPrice(c.getInvestmentCount, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // recordViolation - desc = NewDescriptor("recordViolation", smartcontract.IntegerType, - manifest.NewParameter("violator", smartcontract.Hash160Type), - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("violationType", smartcontract.StringType), - manifest.NewParameter("description", smartcontract.StringType), - manifest.NewParameter("evidenceHash", smartcontract.Hash256Type), - manifest.NewParameter("penalty", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.recordViolation, 1<<17, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // resolveViolation - desc = NewDescriptor("resolveViolation", smartcontract.BoolType, - manifest.NewParameter("violationID", smartcontract.IntegerType), - manifest.NewParameter("resolution", smartcontract.StringType)) - md = NewMethodAndPrice(c.resolveViolation, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // getViolation - desc = NewDescriptor("getViolation", smartcontract.ArrayType, - manifest.NewParameter("violationID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getViolation, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // verifyEmployment - desc = NewDescriptor("verifyEmployment", smartcontract.BoolType, - manifest.NewParameter("employee", smartcontract.Hash160Type), - manifest.NewParameter("employer", smartcontract.Hash160Type), - manifest.NewParameter("position", smartcontract.StringType), - manifest.NewParameter("startDate", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.verifyEmployment, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // revokeEmployment - desc = NewDescriptor("revokeEmployment", smartcontract.BoolType, - manifest.NewParameter("employee", smartcontract.Hash160Type), - manifest.NewParameter("employer", smartcontract.Hash160Type), - manifest.NewParameter("endDate", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.revokeEmployment, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // getEmploymentStatus - desc = NewDescriptor("getEmploymentStatus", smartcontract.ArrayType, - manifest.NewParameter("employee", smartcontract.Hash160Type)) - md = NewMethodAndPrice(c.getEmploymentStatus, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // verifyContractor - desc = NewDescriptor("verifyContractor", smartcontract.BoolType, - manifest.NewParameter("contractor", smartcontract.Hash160Type), - manifest.NewParameter("platform", smartcontract.Hash160Type), - manifest.NewParameter("platformID", smartcontract.StringType), - manifest.NewParameter("contractorID", smartcontract.StringType), - manifest.NewParameter("startDate", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.verifyContractor, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // revokeContractor - desc = NewDescriptor("revokeContractor", smartcontract.BoolType, - manifest.NewParameter("contractor", smartcontract.Hash160Type), - manifest.NewParameter("platform", smartcontract.Hash160Type), - manifest.NewParameter("endDate", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.revokeContractor, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // getContractorStatus - desc = NewDescriptor("getContractorStatus", smartcontract.ArrayType, - manifest.NewParameter("contractor", smartcontract.Hash160Type)) - md = NewMethodAndPrice(c.getContractorStatus, 1<<15, callflag.ReadStates) - c.AddMethod(md, desc) - - // completeInvestmentEducation - desc = NewDescriptor("completeInvestmentEducation", smartcontract.BoolType, - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("certificationID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.completeInvestmentEducation, 1<<16, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // distributeReturns - desc = NewDescriptor("distributeReturns", smartcontract.BoolType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("actualReturns", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.distributeReturns, 1<<18, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // cancelOpportunity - desc = NewDescriptor("cancelOpportunity", smartcontract.BoolType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.cancelOpportunity, 1<<18, callflag.States|callflag.AllowNotify) - c.AddMethod(md, desc) - - // getInvestmentsByOpportunity - desc = NewDescriptor("getInvestmentsByOpportunity", smartcontract.ArrayType, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getInvestmentsByOpportunity, 1<<16, callflag.ReadStates) - c.AddMethod(md, desc) - - // getInvestmentsByInvestor - desc = NewDescriptor("getInvestmentsByInvestor", smartcontract.ArrayType, - manifest.NewParameter("investor", smartcontract.Hash160Type)) - md = NewMethodAndPrice(c.getInvestmentsByInvestor, 1<<16, callflag.ReadStates) - c.AddMethod(md, desc) - - // getOpportunitiesByType - desc = NewDescriptor("getOpportunitiesByType", smartcontract.ArrayType, - manifest.NewParameter("oppType", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getOpportunitiesByType, 1<<16, callflag.ReadStates) - c.AddMethod(md, desc) - - // getOpportunitiesByStatus - desc = NewDescriptor("getOpportunitiesByStatus", smartcontract.ArrayType, - manifest.NewParameter("status", smartcontract.IntegerType)) - md = NewMethodAndPrice(c.getOpportunitiesByStatus, 1<<16, callflag.ReadStates) - c.AddMethod(md, desc) - - // ===== Events ===== - eDesc := NewEventDescriptor(collocatioOpportunityCreatedEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("oppType", smartcontract.IntegerType), - manifest.NewParameter("creator", smartcontract.Hash160Type)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioOpportunityActivatedEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioInvestmentMadeEvent, - manifest.NewParameter("investmentID", smartcontract.IntegerType), - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioInvestmentWithdrawnEvent, - manifest.NewParameter("investmentID", smartcontract.IntegerType), - manifest.NewParameter("returnAmount", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioEligibilityUpdatedEvent, - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("eligibility", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioOpportunityClosedEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioOpportunityFailedEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioOpportunityCancelledEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioReturnsDistributedEvent, - manifest.NewParameter("opportunityID", smartcontract.IntegerType), - manifest.NewParameter("totalReturns", smartcontract.IntegerType), - manifest.NewParameter("investorCount", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioEducationCompletedEvent, - manifest.NewParameter("investor", smartcontract.Hash160Type), - manifest.NewParameter("certificationID", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioViolationRecordedEvent, - manifest.NewParameter("violationID", smartcontract.IntegerType), - manifest.NewParameter("violator", smartcontract.Hash160Type), - manifest.NewParameter("penalty", smartcontract.IntegerType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioViolationResolvedEvent, - manifest.NewParameter("violationID", smartcontract.IntegerType), - manifest.NewParameter("resolution", smartcontract.StringType)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioEmploymentVerifiedEvent, - manifest.NewParameter("employee", smartcontract.Hash160Type), - manifest.NewParameter("employer", smartcontract.Hash160Type)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioEmploymentRevokedEvent, - manifest.NewParameter("employee", smartcontract.Hash160Type), - manifest.NewParameter("employer", smartcontract.Hash160Type)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioContractorVerifiedEvent, - manifest.NewParameter("contractor", smartcontract.Hash160Type), - manifest.NewParameter("platform", smartcontract.Hash160Type)) - c.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(collocatioContractorRevokedEvent, - manifest.NewParameter("contractor", smartcontract.Hash160Type), - manifest.NewParameter("platform", smartcontract.Hash160Type)) - c.AddEvent(NewEvent(eDesc)) - - return c -} - -// Metadata returns contract metadata. -func (c *Collocatio) Metadata() *interop.ContractMD { - return &c.ContractMD -} - -// Initialize initializes Collocatio contract. -func (c *Collocatio) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != c.ActiveIn() { - return nil - } - - // Initialize default config - cfg := state.CollocatioConfig{ - MinPIOParticipants: defaultMinPIOParticipants, - MinEIOParticipants: defaultMinEIOParticipants, - MinCIOParticipants: defaultMinCIOParticipants, - DefaultMinInvestment: defaultMinInvestment, - MaxIndividualCap: defaultMaxIndividualCap, - WealthConcentration: defaultWealthConcentration, - CreationFee: defaultCreationFee, - InvestmentFee: defaultInvestmentFee, - WithdrawalPenalty: defaultWithdrawalPenalty, - MinVotingPeriod: defaultMinVotingPeriod, - MinInvestmentPeriod: defaultMinInvestmentPeriod, - MinMaturityPeriod: defaultMinMaturityPeriod, - MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, - ViolationCooldown: 1000000, - } - c.setConfigInternal(ic.DAO, &cfg) - - return nil -} - -// InitializeCache fills native Collocatio cache from DAO. -func (c *Collocatio) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - return nil -} - -// OnPersist implements the Contract interface. -func (c *Collocatio) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist implements the Contract interface. -// Handles lifecycle automation for opportunities. -func (c *Collocatio) PostPersist(ic *interop.Context) error { - // Run every 100 blocks for performance - if ic.Block.Index%100 != 0 { - return nil - } - - // Process opportunities that need status updates - c.processActiveOpportunities(ic) - c.processClosedOpportunities(ic) - return nil -} - -// processActiveOpportunities handles Active opportunities past their investment deadline. -func (c *Collocatio) processActiveOpportunities(ic *interop.Context) { - prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityActive)} - cfg := c.getConfigInternal(ic.DAO) - - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) < 9 { - return true - } - - oppID := binary.BigEndian.Uint64(k[1:9]) - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - return true - } - - // Check if investment deadline passed - if ic.Block.Index < opp.InvestmentDeadline { - return true - } - - // Get minimum participants for this opportunity type - var minParticipants uint64 - switch opp.Type { - case state.OpportunityPIO: - minParticipants = cfg.MinPIOParticipants - case state.OpportunityEIO: - minParticipants = cfg.MinEIOParticipants - case state.OpportunityCIO: - minParticipants = cfg.MinCIOParticipants - default: - minParticipants = 1 - } - - // Use opportunity's own min if set - if opp.MinParticipants > minParticipants { - minParticipants = opp.MinParticipants - } - - // Check if opportunity met minimum participants - if opp.CurrentParticipants < minParticipants { - // Failed - didn't meet minimum participants - c.updateOpportunityStatus(ic, opp, state.OpportunityFailed) - ic.AddNotification(c.Hash, collocatioOpportunityFailedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - stackitem.NewByteArray([]byte("insufficient participants")), - })) - } else { - // Success - close and move to maturity phase - c.updateOpportunityStatus(ic, opp, state.OpportunityClosed) - ic.AddNotification(c.Hash, collocatioOpportunityClosedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - })) - } - return true - }) -} - -// processClosedOpportunities handles Closed opportunities past their maturity date. -func (c *Collocatio) processClosedOpportunities(ic *interop.Context) { - prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityClosed)} - - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) < 9 { - return true - } - - oppID := binary.BigEndian.Uint64(k[1:9]) - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - return true - } - - // Check if maturity date passed - if ic.Block.Index < opp.MaturityDate { - return true - } - - // Opportunity is mature and ready for returns distribution - // Note: Actual distribution is triggered by distributeReturns call - // This could emit a notification for off-chain systems - // For now, we just log that it's ready (no state change needed) - return true - }) -} - -// updateOpportunityStatus updates an opportunity's status and maintains status index. -func (c *Collocatio) updateOpportunityStatus(ic *interop.Context, opp *state.InvestmentOpportunity, newStatus state.OpportunityStatus) { - oldStatus := opp.Status - - // Remove from old status index - oldStatusKey := makeCollocatioOppByStatusKey(oldStatus, opp.ID) - ic.DAO.DeleteStorageItem(c.ID, oldStatusKey) - - // Update status - opp.Status = newStatus - opp.UpdatedAt = ic.Block.Index - - // Add to new status index - newStatusKey := makeCollocatioOppByStatusKey(newStatus, opp.ID) - ic.DAO.PutStorageItem(c.ID, newStatusKey, []byte{1}) - - // Save opportunity - c.putOpportunity(ic.DAO, opp) -} - -// ActiveIn returns nil (always active from genesis). -func (c *Collocatio) ActiveIn() *config.Hardfork { - return nil -} - -// ============================================================================ -// Storage Key Helpers -// ============================================================================ - -func makeCollocatioConfigKey() []byte { - return []byte{collocatioPrefixConfig} -} - -func makeCollocatioOppKey(oppID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixOpportunity - binary.BigEndian.PutUint64(key[1:], oppID) - return key -} - -func makeCollocatioOppByTypeKey(oppType state.OpportunityType, oppID uint64) []byte { - key := make([]byte, 10) - key[0] = collocatioPrefixOpportunityByType - key[1] = byte(oppType) - binary.BigEndian.PutUint64(key[2:], oppID) - return key -} - -func makeCollocatioOppByStatusKey(status state.OpportunityStatus, oppID uint64) []byte { - key := make([]byte, 10) - key[0] = collocatioPrefixOpportunityByStatus - key[1] = byte(status) - binary.BigEndian.PutUint64(key[2:], oppID) - return key -} - -func makeCollocatioOppCounterKey() []byte { - return []byte{collocatioPrefixOppCounter} -} - -func makeCollocatioInvKey(invID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixInvestment - binary.BigEndian.PutUint64(key[1:], invID) - return key -} - -func makeCollocatioInvByOppKey(oppID, invID uint64) []byte { - key := make([]byte, 17) - key[0] = collocatioPrefixInvestmentByOpp - binary.BigEndian.PutUint64(key[1:9], oppID) - binary.BigEndian.PutUint64(key[9:], invID) - return key -} - -func makeCollocatioInvByInvestorKey(vitaID, invID uint64) []byte { - key := make([]byte, 17) - key[0] = collocatioPrefixInvestmentByInvestor - binary.BigEndian.PutUint64(key[1:9], vitaID) - binary.BigEndian.PutUint64(key[9:], invID) - return key -} - -func makeCollocatioInvCounterKey() []byte { - return []byte{collocatioPrefixInvCounter} -} - -func makeCollocatioEligKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixEligibility - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -func makeCollocatioEligByOwnerKey(owner util.Uint160) []byte { - key := make([]byte, 1+util.Uint160Size) - key[0] = collocatioPrefixEligibilityByOwner - copy(key[1:], owner.BytesBE()) - return key -} - -func makeCollocatioViolationKey(violationID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixViolation - binary.BigEndian.PutUint64(key[1:], violationID) - return key -} - -func makeCollocatioViolationByInvestorKey(vitaID, violationID uint64) []byte { - key := make([]byte, 17) - key[0] = collocatioPrefixViolationByInvestor - binary.BigEndian.PutUint64(key[1:9], vitaID) - binary.BigEndian.PutUint64(key[9:], violationID) - return key -} - -func makeCollocatioViolationCounterKey() []byte { - return []byte{collocatioPrefixViolationCounter} -} - -func makeCollocatioEmploymentKey(employeeVitaID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixEmployment - binary.BigEndian.PutUint64(key[1:], employeeVitaID) - return key -} - -func makeCollocatioEmploymentByEmployerKey(employerVitaID, employeeVitaID uint64) []byte { - key := make([]byte, 17) - key[0] = collocatioPrefixEmploymentByEmployer - binary.BigEndian.PutUint64(key[1:9], employerVitaID) - binary.BigEndian.PutUint64(key[9:], employeeVitaID) - return key -} - -func makeCollocatioContractorKey(contractorVitaID uint64) []byte { - key := make([]byte, 9) - key[0] = collocatioPrefixContractor - binary.BigEndian.PutUint64(key[1:], contractorVitaID) - return key -} - -func makeCollocatioContractorByPlatformKey(platform util.Uint160, contractorVitaID uint64) []byte { - key := make([]byte, 1+util.Uint160Size+8) - key[0] = collocatioPrefixContractorByPlatform - copy(key[1:1+util.Uint160Size], platform.BytesBE()) - binary.BigEndian.PutUint64(key[1+util.Uint160Size:], contractorVitaID) - return key -} - -// ============================================================================ -// Internal Storage Methods -// ============================================================================ - -func (c *Collocatio) getConfigInternal(d *dao.Simple) *state.CollocatioConfig { - si := d.GetStorageItem(c.ID, makeCollocatioConfigKey()) - if si == nil { - return &state.CollocatioConfig{ - MinPIOParticipants: defaultMinPIOParticipants, - MinEIOParticipants: defaultMinEIOParticipants, - MinCIOParticipants: defaultMinCIOParticipants, - DefaultMinInvestment: defaultMinInvestment, - MaxIndividualCap: defaultMaxIndividualCap, - WealthConcentration: defaultWealthConcentration, - CreationFee: defaultCreationFee, - InvestmentFee: defaultInvestmentFee, - WithdrawalPenalty: defaultWithdrawalPenalty, - MinVotingPeriod: defaultMinVotingPeriod, - MinInvestmentPeriod: defaultMinInvestmentPeriod, - MinMaturityPeriod: defaultMinMaturityPeriod, - MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, - ViolationCooldown: 1000000, - } - } - cfg := new(state.CollocatioConfig) - item, _ := stackitem.Deserialize(si) - cfg.FromStackItem(item) - return cfg -} - -func (c *Collocatio) setConfigInternal(d *dao.Simple, cfg *state.CollocatioConfig) { - item, _ := cfg.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioConfigKey(), data) -} - -func (c *Collocatio) getCounter(d *dao.Simple, key []byte) uint64 { - si := d.GetStorageItem(c.ID, key) - if si == nil || len(si) < 8 { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (c *Collocatio) incrementCounter(d *dao.Simple, key []byte) uint64 { - current := c.getCounter(d, key) - next := current + 1 - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, next) - d.PutStorageItem(c.ID, key, buf) - return next -} - -func (c *Collocatio) getOpportunityInternal(d *dao.Simple, oppID uint64) *state.InvestmentOpportunity { - si := d.GetStorageItem(c.ID, makeCollocatioOppKey(oppID)) - if si == nil { - return nil - } - opp := new(state.InvestmentOpportunity) - item, _ := stackitem.Deserialize(si) - opp.FromStackItem(item) - return opp -} - -func (c *Collocatio) putOpportunity(d *dao.Simple, opp *state.InvestmentOpportunity) { - item, _ := opp.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioOppKey(opp.ID), data) -} - -func (c *Collocatio) getInvestmentInternal(d *dao.Simple, invID uint64) *state.Investment { - si := d.GetStorageItem(c.ID, makeCollocatioInvKey(invID)) - if si == nil { - return nil - } - inv := new(state.Investment) - item, _ := stackitem.Deserialize(si) - inv.FromStackItem(item) - return inv -} - -func (c *Collocatio) putInvestment(d *dao.Simple, inv *state.Investment) { - item, _ := inv.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioInvKey(inv.ID), data) -} - -func (c *Collocatio) getEligibilityInternal(d *dao.Simple, investor util.Uint160) *state.InvestorEligibility { - // First get vitaID from owner mapping - si := d.GetStorageItem(c.ID, makeCollocatioEligByOwnerKey(investor)) - if si == nil || len(si) < 8 { - return nil - } - vitaID := binary.BigEndian.Uint64(si) - - // Then get eligibility - eligSI := d.GetStorageItem(c.ID, makeCollocatioEligKey(vitaID)) - if eligSI == nil { - return nil - } - elig := new(state.InvestorEligibility) - item, _ := stackitem.Deserialize(eligSI) - elig.FromStackItem(item) - return elig -} - -func (c *Collocatio) putEligibility(d *dao.Simple, elig *state.InvestorEligibility) { - item, _ := elig.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioEligKey(elig.VitaID), data) - - // Also store owner -> vitaID mapping - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, elig.VitaID) - d.PutStorageItem(c.ID, makeCollocatioEligByOwnerKey(elig.Investor), buf) -} - -// ============================================================================ -// Contract Methods -// ============================================================================ - -func (c *Collocatio) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - cfg := c.getConfigInternal(ic.DAO) - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinPIOParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinEIOParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinCIOParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.DefaultMinInvestment)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxIndividualCap)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WealthConcentration)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.CreationFee)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.InvestmentFee)), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WithdrawalPenalty)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinVotingPeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinInvestmentPeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinMaturityPeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MaxViolationsBeforeBan))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.ViolationCooldown))), - }) -} - -func (c *Collocatio) getOpportunityCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := c.getCounter(ic.DAO, makeCollocatioOppCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -func (c *Collocatio) getOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - return stackitem.Null{} - } - return opportunityToStackItem(opp) -} - -func (c *Collocatio) createOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - caller := ic.VM.GetCallingScriptHash() - - oppType := state.OpportunityType(toUint64(args[0])) - name := toString(args[1]) - description := toString(args[2]) - termsHashBytes, err := args[3].TryBytes() - if err != nil { - panic(err) - } - termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes) - if err != nil { - panic(err) - } - minParticipants := toUint64(args[4]) - maxParticipants := toUint64(args[5]) - minInvestment := toUint64(args[6]) - maxInvestment := toUint64(args[7]) - targetPool := toUint64(args[8]) - expectedReturns := toUint64(args[9]) - riskLevel := uint8(toUint64(args[10])) - maturityBlocks := uint32(toUint64(args[11])) - - // Validate caller has Vita - vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) - if err != nil { - panic("caller must have Vita token") - } - vitaID := vita.TokenID - - // Validate opportunity type - if oppType > state.OpportunityCIO { - panic("invalid opportunity type") - } - - // Validate parameters - cfg := c.getConfigInternal(ic.DAO) - if maturityBlocks < cfg.MinMaturityPeriod { - panic("maturity period too short") - } - if riskLevel < 1 || riskLevel > 10 { - panic("risk level must be 1-10") - } - - // Get minimum participants based on type - var minRequired uint64 - switch oppType { - case state.OpportunityPIO: - minRequired = cfg.MinPIOParticipants - case state.OpportunityEIO: - minRequired = cfg.MinEIOParticipants - case state.OpportunityCIO: - minRequired = cfg.MinCIOParticipants - } - if minParticipants < minRequired { - panic(fmt.Sprintf("minimum participants must be at least %d for this type", minRequired)) - } - - // Charge creation fee to Treasury - if cfg.CreationFee > 0 { - if err := c.VTS.transferUnrestricted(ic, caller, nativehashes.Treasury, new(big.Int).SetUint64(cfg.CreationFee), nil); err != nil { - panic("failed to pay creation fee") - } - } - - // Create opportunity - oppID := c.incrementCounter(ic.DAO, makeCollocatioOppCounterKey()) - currentBlock := ic.Block.Index - - opp := &state.InvestmentOpportunity{ - ID: oppID, - Type: oppType, - Status: state.OpportunityDraft, - Creator: caller, - SponsorVitaID: vitaID, - Name: name, - Description: description, - TermsHash: termsHash, - MinParticipants: minParticipants, - MaxParticipants: maxParticipants, - CurrentParticipants: 0, - MinInvestment: minInvestment, - MaxInvestment: maxInvestment, - TotalPool: 0, - TargetPool: targetPool, - ExpectedReturns: expectedReturns, - RiskLevel: riskLevel, - VotingDeadline: currentBlock + cfg.MinVotingPeriod, - InvestmentDeadline: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod, - MaturityDate: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod + maturityBlocks, - ProposalID: 0, - CreatedAt: currentBlock, - UpdatedAt: currentBlock, - } - - c.putOpportunity(ic.DAO, opp) - - // Store type index - ic.DAO.PutStorageItem(c.ID, makeCollocatioOppByTypeKey(oppType, oppID), []byte{1}) - - // Emit event - ic.AddNotification(c.Hash, collocatioOpportunityCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(oppType))), - stackitem.NewByteArray(caller.BytesBE()), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)) -} - -func (c *Collocatio) activateOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - panic("opportunity not found") - } - - caller := ic.VM.GetCallingScriptHash() - if caller != opp.Creator && !c.Tutus.CheckCommittee(ic) { - panic("only creator or committee can activate") - } - - if opp.Status != state.OpportunityDraft { - panic("opportunity must be in draft status") - } - - opp.Status = state.OpportunityActive - opp.UpdatedAt = ic.Block.Index - c.putOpportunity(ic.DAO, opp) - - ic.AddNotification(c.Hash, collocatioOpportunityActivatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) invest(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - amount := toUint64(args[1]) - caller := ic.VM.GetCallingScriptHash() - - // Validate caller has Vita - vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) - if err != nil { - panic("caller must have Vita token") - } - vitaID := vita.TokenID - - // Get opportunity - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - panic("opportunity not found") - } - - if opp.Status != state.OpportunityActive { - panic("opportunity is not active") - } - if ic.Block.Index > opp.InvestmentDeadline { - panic("investment deadline has passed") - } - - if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants { - panic("maximum participants reached") - } - - if amount < opp.MinInvestment { - panic("investment below minimum") - } - if amount > opp.MaxInvestment { - panic("investment exceeds maximum") - } - - // Check eligibility - if !c.isEligibleInternal(ic.DAO, caller, opp.Type) { - panic("investor not eligible for this opportunity type") - } - - // Calculate fee - cfg := c.getConfigInternal(ic.DAO) - fee := (amount * cfg.InvestmentFee) / 10000 - netAmount := amount - fee - - // Transfer VTS from investor - if err := c.VTS.transferUnrestricted(ic, caller, c.Hash, new(big.Int).SetUint64(amount), nil); err != nil { - panic("failed to transfer investment amount") - } - - // Send fee to Treasury - if fee > 0 { - if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil { - panic("failed to transfer fee to treasury") - } - } - - // Create investment record - invID := c.incrementCounter(ic.DAO, makeCollocatioInvCounterKey()) - - inv := &state.Investment{ - ID: invID, - OpportunityID: oppID, - VitaID: vitaID, - Investor: caller, - Amount: netAmount, - Status: state.InvestmentActive, - ReturnAmount: 0, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - - c.putInvestment(ic.DAO, inv) - - // Store indexes - ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByOppKey(oppID, invID), []byte{1}) - ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByInvestorKey(vitaID, invID), []byte{1}) - - // Update opportunity - opp.CurrentParticipants++ - opp.TotalPool += netAmount - opp.UpdatedAt = ic.Block.Index - c.putOpportunity(ic.DAO, opp) - - // Update eligibility stats - c.updateEligibilityOnInvest(ic.DAO, caller, vitaID, netAmount, ic.Block.Index) - - // Emit event - ic.AddNotification(c.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - stackitem.NewByteArray(caller.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(invID)) -} - -func (c *Collocatio) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item { - invID := toUint64(args[0]) - caller := ic.VM.GetCallingScriptHash() - - inv := c.getInvestmentInternal(ic.DAO, invID) - if inv == nil { - panic("investment not found") - } - - if inv.Investor != caller { - panic("only investor can withdraw") - } - - if inv.Status != state.InvestmentActive { - panic("investment is not active") - } - - opp := c.getOpportunityInternal(ic.DAO, inv.OpportunityID) - if opp == nil { - panic("opportunity not found") - } - - // Calculate penalty for early withdrawal - cfg := c.getConfigInternal(ic.DAO) - returnAmount := inv.Amount - if ic.Block.Index < opp.MaturityDate && cfg.WithdrawalPenalty > 0 { - penalty := (inv.Amount * cfg.WithdrawalPenalty) / 10000 - returnAmount = inv.Amount - penalty - if penalty > 0 { - if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { - panic("failed to transfer penalty") - } - } - } - - // Return funds - if err := c.VTS.transferUnrestricted(ic, c.Hash, caller, new(big.Int).SetUint64(returnAmount), nil); err != nil { - panic("failed to return investment") - } - - // Update investment - inv.Status = state.InvestmentWithdrawn - inv.UpdatedAt = ic.Block.Index - c.putInvestment(ic.DAO, inv) - - // Update opportunity - opp.CurrentParticipants-- - opp.TotalPool -= inv.Amount - opp.UpdatedAt = ic.Block.Index - c.putOpportunity(ic.DAO, opp) - - // Update eligibility - c.updateEligibilityOnWithdraw(ic.DAO, caller, inv.Amount, ic.Block.Index) - - // Emit event - ic.AddNotification(c.Hash, collocatioInvestmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) getEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { - investor := toUint160(args[0]) - elig := c.getEligibilityInternal(ic.DAO, investor) - if elig == nil { - return stackitem.Null{} - } - return eligibilityToStackItem(elig) -} - -func (c *Collocatio) setEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { - investor := toUint160(args[0]) - eligFlags := state.EligibilityType(toUint64(args[1])) - - // Only committee or RoleInvestmentManager can set eligibility - if !c.Tutus.CheckCommittee(ic) { - caller := ic.VM.GetCallingScriptHash() - if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { - panic("only committee or investment manager can set eligibility") - } - } - - vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) - if err != nil { - panic("investor must have Vita token") - } - vitaID := vita.TokenID - - elig := c.getEligibilityInternal(ic.DAO, investor) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: vitaID, - Investor: investor, - CreatedAt: ic.Block.Index, - } - } - - elig.Eligibility = eligFlags - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - - ic.AddNotification(c.Hash, collocatioEligibilityUpdatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(investor.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(eligFlags))), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) isEligible(ic *interop.Context, args []stackitem.Item) stackitem.Item { - investor := toUint160(args[0]) - oppType := state.OpportunityType(toUint64(args[1])) - return stackitem.NewBool(c.isEligibleInternal(ic.DAO, investor, oppType)) -} - -func (c *Collocatio) isEligibleInternal(d *dao.Simple, investor util.Uint160, oppType state.OpportunityType) bool { - elig := c.getEligibilityInternal(d, investor) - if elig == nil { - return false - } - - // Must have completed investment education - if !elig.ScireCompleted { - return false - } - - // Check for ban - if elig.HasViolations { - cfg := c.getConfigInternal(d) - if elig.ViolationCount >= cfg.MaxViolationsBeforeBan { - return false - } - } - - switch oppType { - case state.OpportunityPIO: - return elig.Eligibility&state.EligibilityPIO != 0 - case state.OpportunityEIO: - if elig.Eligibility&state.EligibilityEIO == 0 { - return false - } - // Additionally verify active employment - return c.hasActiveEmployment(d, elig.VitaID) - case state.OpportunityCIO: - if elig.Eligibility&state.EligibilityCIO == 0 { - return false - } - // Additionally verify active contractor status - return c.hasActiveContractor(d, elig.VitaID) - default: - return false - } -} - -func (c *Collocatio) getInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - invID := toUint64(args[0]) - inv := c.getInvestmentInternal(ic.DAO, invID) - if inv == nil { - return stackitem.Null{} - } - return investmentToStackItem(inv) -} - -func (c *Collocatio) getInvestmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := c.getCounter(ic.DAO, makeCollocatioInvCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -// ============================================================================ -// Internal Helpers -// ============================================================================ - -func (c *Collocatio) updateEligibilityOnInvest(d *dao.Simple, investor util.Uint160, vitaID, amount uint64, blockHeight uint32) { - elig := c.getEligibilityInternal(d, investor) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: vitaID, - Investor: investor, - CreatedAt: blockHeight, - } - } - elig.TotalInvested += amount - elig.ActiveInvestments++ - elig.LastActivity = blockHeight - elig.UpdatedAt = blockHeight - c.putEligibility(d, elig) -} - -func (c *Collocatio) updateEligibilityOnWithdraw(d *dao.Simple, investor util.Uint160, amount uint64, blockHeight uint32) { - elig := c.getEligibilityInternal(d, investor) - if elig == nil { - return - } - if elig.TotalInvested >= amount { - elig.TotalInvested -= amount - } - if elig.ActiveInvestments > 0 { - elig.ActiveInvestments-- - } - elig.LastActivity = blockHeight - elig.UpdatedAt = blockHeight - c.putEligibility(d, elig) -} - -// ============================================================================ -// Violation System -// ============================================================================ - -func (c *Collocatio) getViolationInternal(d *dao.Simple, violationID uint64) *state.InvestmentViolation { - si := d.GetStorageItem(c.ID, makeCollocatioViolationKey(violationID)) - if si == nil { - return nil - } - v := new(state.InvestmentViolation) - item, _ := stackitem.Deserialize(si) - v.FromStackItem(item) - return v -} - -func (c *Collocatio) putViolation(d *dao.Simple, v *state.InvestmentViolation) { - item, _ := v.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioViolationKey(v.ID), data) -} - -func (c *Collocatio) recordViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { - violator := toUint160(args[0]) - opportunityID := toUint64(args[1]) - violationType := toString(args[2]) - description := toString(args[3]) - evidenceHashBytes, err := args[4].TryBytes() - if err != nil { - panic(err) - } - evidenceHash, err := util.Uint256DecodeBytesBE(evidenceHashBytes) - if err != nil { - panic(err) - } - penalty := toUint64(args[5]) - - // Authorization: Committee or RoleInvestmentManager - if !c.Tutus.CheckCommittee(ic) { - caller := ic.VM.GetCallingScriptHash() - if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { - panic("only committee or investment manager can record violations") - } - } - - // Verify violator has Vita - vita, err := c.Vita.GetTokenByOwner(ic.DAO, violator) - if err != nil || vita == nil { - panic("violator must have active Vita") - } - vitaID := vita.TokenID - - // Create violation record - violationID := c.incrementCounter(ic.DAO, makeCollocatioViolationCounterKey()) - caller := ic.VM.GetCallingScriptHash() - - v := &state.InvestmentViolation{ - ID: violationID, - VitaID: vitaID, - Violator: violator, - OpportunityID: opportunityID, - ViolationType: violationType, - Description: description, - EvidenceHash: evidenceHash, - Penalty: penalty, - ReportedBy: caller, - ReportedAt: ic.Block.Index, - ResolvedAt: 0, - Resolution: "", - } - - c.putViolation(ic.DAO, v) - - // Store index by investor - ic.DAO.PutStorageItem(c.ID, makeCollocatioViolationByInvestorKey(vitaID, violationID), []byte{1}) - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, violator) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: vitaID, - Investor: violator, - CreatedAt: ic.Block.Index, - } - } - elig.HasViolations = true - elig.ViolationCount++ - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - - // Apply penalty if specified - if penalty > 0 { - if err := c.VTS.transferUnrestricted(ic, violator, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { - // Don't panic if transfer fails, just record violation without penalty - } - } - - // Emit event - ic.AddNotification(c.Hash, collocatioViolationRecordedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), - stackitem.NewByteArray(violator.BytesBE()), - stackitem.NewByteArray([]byte(violationType)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)) -} - -func (c *Collocatio) resolveViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { - violationID := toUint64(args[0]) - resolution := toString(args[1]) - - // Authorization: Committee or RoleInvestmentManager - if !c.Tutus.CheckCommittee(ic) { - caller := ic.VM.GetCallingScriptHash() - if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { - panic("only committee or investment manager can resolve violations") - } - } - - v := c.getViolationInternal(ic.DAO, violationID) - if v == nil { - panic("violation not found") - } - if v.ResolvedAt != 0 { - panic("violation already resolved") - } - - v.ResolvedAt = ic.Block.Index - v.Resolution = resolution - c.putViolation(ic.DAO, v) - - // Emit event - ic.AddNotification(c.Hash, collocatioViolationResolvedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), - stackitem.NewByteArray([]byte(resolution)), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) getViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { - violationID := toUint64(args[0]) - v := c.getViolationInternal(ic.DAO, violationID) - if v == nil { - return stackitem.Null{} - } - return violationToStackItem(v) -} - -// ============================================================================ -// Employment Verification (EIO) -// ============================================================================ - -func (c *Collocatio) getEmploymentInternal(d *dao.Simple, employeeVitaID uint64) *state.EmploymentVerification { - si := d.GetStorageItem(c.ID, makeCollocatioEmploymentKey(employeeVitaID)) - if si == nil { - return nil - } - ev := new(state.EmploymentVerification) - item, _ := stackitem.Deserialize(si) - ev.FromStackItem(item) - return ev -} - -func (c *Collocatio) putEmployment(d *dao.Simple, ev *state.EmploymentVerification) { - item, _ := ev.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioEmploymentKey(ev.VitaID), data) -} - -func (c *Collocatio) verifyEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - employee := toUint160(args[0]) - employer := toUint160(args[1]) - position := toString(args[2]) - startDate := uint32(toUint64(args[3])) - - caller := ic.VM.GetCallingScriptHash() - - // Authorization: Committee, RoleInvestmentManager, or employer - isAuthorized := c.Tutus.CheckCommittee(ic) || - c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || - caller == employer - - if !isAuthorized { - panic("only committee, investment manager, or employer can verify employment") - } - - // Verify both employee and employer have Vita - employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) - if err != nil { - panic("employee must have Vita token") - } - employerVita, err := c.Vita.GetTokenByOwner(ic.DAO, employer) - if err != nil { - panic("employer must have Vita token") - } - - ev := &state.EmploymentVerification{ - VitaID: employeeVita.TokenID, - Employee: employee, - EmployerVitaID: employerVita.TokenID, - Employer: employer, - Position: position, - StartDate: startDate, - EndDate: 0, - IsActive: true, - VerifiedAt: ic.Block.Index, - VerifiedBy: caller, - } - - c.putEmployment(ic.DAO, ev) - - // Store index by employer - ic.DAO.PutStorageItem(c.ID, makeCollocatioEmploymentByEmployerKey(employerVita.TokenID, employeeVita.TokenID), []byte{1}) - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, employee) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: employeeVita.TokenID, - Investor: employee, - CreatedAt: ic.Block.Index, - } - } - elig.Eligibility |= state.EligibilityEIO - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - - // Emit event - ic.AddNotification(c.Hash, collocatioEmploymentVerifiedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(employee.BytesBE()), - stackitem.NewByteArray(employer.BytesBE()), - stackitem.NewByteArray([]byte(position)), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) revokeEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - employee := toUint160(args[0]) - employer := toUint160(args[1]) - endDate := uint32(toUint64(args[2])) - - caller := ic.VM.GetCallingScriptHash() - - // Authorization: Committee, RoleInvestmentManager, or employer - isAuthorized := c.Tutus.CheckCommittee(ic) || - c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || - caller == employer - - if !isAuthorized { - panic("only committee, investment manager, or employer can revoke employment") - } - - // Get employee Vita - employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) - if err != nil { - panic("employee must have Vita token") - } - - ev := c.getEmploymentInternal(ic.DAO, employeeVita.TokenID) - if ev == nil { - panic("employment not found") - } - if !ev.IsActive { - panic("employment already revoked") - } - if ev.Employer != employer { - panic("employer mismatch") - } - - ev.IsActive = false - ev.EndDate = endDate - c.putEmployment(ic.DAO, ev) - - // Remove EIO eligibility - elig := c.getEligibilityInternal(ic.DAO, employee) - if elig != nil { - elig.Eligibility &^= state.EligibilityEIO - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - } - - // Emit event - ic.AddNotification(c.Hash, collocatioEmploymentRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(employee.BytesBE()), - stackitem.NewByteArray(employer.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) getEmploymentStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { - employee := toUint160(args[0]) - - vita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) - if err != nil { - return stackitem.Null{} - } - - ev := c.getEmploymentInternal(ic.DAO, vita.TokenID) - if ev == nil { - return stackitem.Null{} - } - return employmentToStackItem(ev) -} - -func (c *Collocatio) hasActiveEmployment(d *dao.Simple, vitaID uint64) bool { - ev := c.getEmploymentInternal(d, vitaID) - return ev != nil && ev.IsActive -} - -// ============================================================================ -// Contractor Verification (CIO) -// ============================================================================ - -func (c *Collocatio) getContractorInternal(d *dao.Simple, contractorVitaID uint64) *state.ContractorVerification { - si := d.GetStorageItem(c.ID, makeCollocatioContractorKey(contractorVitaID)) - if si == nil { - return nil - } - cv := new(state.ContractorVerification) - item, _ := stackitem.Deserialize(si) - cv.FromStackItem(item) - return cv -} - -func (c *Collocatio) putContractor(d *dao.Simple, cv *state.ContractorVerification) { - item, _ := cv.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(c.ID, makeCollocatioContractorKey(cv.VitaID), data) -} - -func (c *Collocatio) verifyContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { - contractor := toUint160(args[0]) - platform := toUint160(args[1]) - platformID := toString(args[2]) - contractorID := toString(args[3]) - startDate := uint32(toUint64(args[4])) - - caller := ic.VM.GetCallingScriptHash() - - // Authorization: Committee, RoleInvestmentManager, or platform - isAuthorized := c.Tutus.CheckCommittee(ic) || - c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || - caller == platform - - if !isAuthorized { - panic("only committee, investment manager, or platform can verify contractor") - } - - // Verify contractor has Vita - contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) - if err != nil { - panic("contractor must have Vita token") - } - - cv := &state.ContractorVerification{ - VitaID: contractorVita.TokenID, - Contractor: contractor, - PlatformID: platformID, - Platform: platform, - ContractorID: contractorID, - StartDate: startDate, - EndDate: 0, - IsActive: true, - VerifiedAt: ic.Block.Index, - VerifiedBy: caller, - } - - c.putContractor(ic.DAO, cv) - - // Store index by platform - ic.DAO.PutStorageItem(c.ID, makeCollocatioContractorByPlatformKey(platform, contractorVita.TokenID), []byte{1}) - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, contractor) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: contractorVita.TokenID, - Investor: contractor, - CreatedAt: ic.Block.Index, - } - } - elig.Eligibility |= state.EligibilityCIO - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - - // Emit event - ic.AddNotification(c.Hash, collocatioContractorVerifiedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(contractor.BytesBE()), - stackitem.NewByteArray(platform.BytesBE()), - stackitem.NewByteArray([]byte(platformID)), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) revokeContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { - contractor := toUint160(args[0]) - platform := toUint160(args[1]) - endDate := uint32(toUint64(args[2])) - - caller := ic.VM.GetCallingScriptHash() - - // Authorization: Committee, RoleInvestmentManager, or platform - isAuthorized := c.Tutus.CheckCommittee(ic) || - c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || - caller == platform - - if !isAuthorized { - panic("only committee, investment manager, or platform can revoke contractor") - } - - // Get contractor Vita - contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) - if err != nil { - panic("contractor must have Vita token") - } - - cv := c.getContractorInternal(ic.DAO, contractorVita.TokenID) - if cv == nil { - panic("contractor not found") - } - if !cv.IsActive { - panic("contractor already revoked") - } - if cv.Platform != platform { - panic("platform mismatch") - } - - cv.IsActive = false - cv.EndDate = endDate - c.putContractor(ic.DAO, cv) - - // Remove CIO eligibility - elig := c.getEligibilityInternal(ic.DAO, contractor) - if elig != nil { - elig.Eligibility &^= state.EligibilityCIO - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - } - - // Emit event - ic.AddNotification(c.Hash, collocatioContractorRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(contractor.BytesBE()), - stackitem.NewByteArray(platform.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) getContractorStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { - contractor := toUint160(args[0]) - - vita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) - if err != nil { - return stackitem.Null{} - } - - cv := c.getContractorInternal(ic.DAO, vita.TokenID) - if cv == nil { - return stackitem.Null{} - } - return contractorToStackItem(cv) -} - -func (c *Collocatio) hasActiveContractor(d *dao.Simple, vitaID uint64) bool { - cv := c.getContractorInternal(d, vitaID) - return cv != nil && cv.IsActive -} - -// ============================================================================ -// Education Completion -// ============================================================================ - -func (c *Collocatio) completeInvestmentEducation(ic *interop.Context, args []stackitem.Item) stackitem.Item { - investor := toUint160(args[0]) - - caller := ic.VM.GetCallingScriptHash() - - // Authorization: Committee or RoleInvestmentManager - if !c.Tutus.CheckCommittee(ic) { - if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { - panic("only committee or investment manager can complete education") - } - } - - // Verify investor has Vita - vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) - if err != nil { - panic("investor must have Vita token") - } - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, investor) - if elig == nil { - elig = &state.InvestorEligibility{ - VitaID: vita.TokenID, - Investor: investor, - CreatedAt: ic.Block.Index, - } - } - elig.ScireCompleted = true - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - - // Emit event - ic.AddNotification(c.Hash, collocatioEducationCompletedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewByteArray(investor.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -// ============================================================================ -// Returns Distribution & Cancellation -// ============================================================================ - -func (c *Collocatio) distributeReturns(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - actualReturns := toUint64(args[1]) - - // Authorization: Committee or RoleInvestmentManager - if !c.Tutus.CheckCommittee(ic) { - caller := ic.VM.GetCallingScriptHash() - if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { - panic("only committee or investment manager can distribute returns") - } - } - - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - panic("opportunity not found") - } - if opp.Status != state.OpportunityClosed { - panic("opportunity must be in closed status") - } - if ic.Block.Index < opp.MaturityDate { - panic("maturity date not reached") - } - if opp.TotalPool == 0 { - panic("no investments to distribute") - } - - // Collect all investment IDs first (to avoid modifying during iteration) - prefix := []byte{collocatioPrefixInvestmentByOpp} - prefix = append(prefix, make([]byte, 8)...) - binary.BigEndian.PutUint64(prefix[1:], oppID) - - var invIDs []uint64 - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 16 { - invID := binary.BigEndian.Uint64(k[8:16]) - invIDs = append(invIDs, invID) - } - return true - }) - - // Process each investment - for _, invID := range invIDs { - inv := c.getInvestmentInternal(ic.DAO, invID) - if inv == nil || inv.Status != state.InvestmentActive { - continue - } - - // Calculate proportional return - returnAmount := (inv.Amount * actualReturns) / opp.TotalPool - - // Transfer return to investor - if returnAmount > 0 { - if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(returnAmount), nil); err != nil { - continue // Skip failed transfers - } - } - - // Also return principal - if inv.Amount > 0 { - if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { - continue - } - } - - // Update investment - inv.ReturnAmount = returnAmount - inv.Status = state.InvestmentCompleted - inv.UpdatedAt = ic.Block.Index - c.putInvestment(ic.DAO, inv) - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, inv.Investor) - if elig != nil { - elig.TotalReturns += returnAmount - elig.CompletedInvestments++ - if elig.ActiveInvestments > 0 { - elig.ActiveInvestments-- - } - if elig.TotalInvested >= inv.Amount { - elig.TotalInvested -= inv.Amount - } - elig.LastActivity = ic.Block.Index - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - } - - // Emit event for each distribution - ic.AddNotification(c.Hash, collocatioReturnsDistributedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), - stackitem.NewByteArray(inv.Investor.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), - })) - } - - // Update opportunity status - opp.Status = state.OpportunityCompleted - opp.UpdatedAt = ic.Block.Index - c.putOpportunity(ic.DAO, opp) - - return stackitem.NewBool(true) -} - -func (c *Collocatio) cancelOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - caller := ic.VM.GetCallingScriptHash() - - opp := c.getOpportunityInternal(ic.DAO, oppID) - if opp == nil { - panic("opportunity not found") - } - - // Authorization: Creator (if Draft/Voting) or Committee - isCreator := caller == opp.Creator - isCommittee := c.Tutus.CheckCommittee(ic) - - if opp.Status == state.OpportunityDraft || opp.Status == state.OpportunityVoting { - if !isCreator && !isCommittee { - panic("only creator or committee can cancel draft/voting opportunity") - } - } else if opp.Status == state.OpportunityActive { - if !isCommittee { - panic("only committee can cancel active opportunity") - } - } else { - panic("opportunity cannot be cancelled in current status") - } - - // Collect all investment IDs first (to avoid modifying during iteration) - prefix := []byte{collocatioPrefixInvestmentByOpp} - prefix = append(prefix, make([]byte, 8)...) - binary.BigEndian.PutUint64(prefix[1:], oppID) - - var invIDs []uint64 - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 16 { - invID := binary.BigEndian.Uint64(k[8:16]) - invIDs = append(invIDs, invID) - } - return true - }) - - // Process each investment - for _, invID := range invIDs { - inv := c.getInvestmentInternal(ic.DAO, invID) - if inv == nil || inv.Status != state.InvestmentActive { - continue - } - - // Refund full amount - if inv.Amount > 0 { - if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { - continue - } - } - - // Update investment - inv.Status = state.InvestmentRefunded - inv.UpdatedAt = ic.Block.Index - c.putInvestment(ic.DAO, inv) - - // Update eligibility - elig := c.getEligibilityInternal(ic.DAO, inv.Investor) - if elig != nil { - if elig.ActiveInvestments > 0 { - elig.ActiveInvestments-- - } - if elig.TotalInvested >= inv.Amount { - elig.TotalInvested -= inv.Amount - } - elig.LastActivity = ic.Block.Index - elig.UpdatedAt = ic.Block.Index - c.putEligibility(ic.DAO, elig) - } - } - - // Update opportunity status - opp.Status = state.OpportunityCancelled - opp.UpdatedAt = ic.Block.Index - c.putOpportunity(ic.DAO, opp) - - // Emit event - ic.AddNotification(c.Hash, collocatioOpportunityCancelledEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), - })) - - return stackitem.NewBool(true) -} - -// ============================================================================ -// Query Methods -// ============================================================================ - -func (c *Collocatio) getInvestmentsByOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppID := toUint64(args[0]) - - prefix := []byte{collocatioPrefixInvestmentByOpp} - prefix = append(prefix, make([]byte, 8)...) - binary.BigEndian.PutUint64(prefix[1:], oppID) - - var ids []stackitem.Item - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 16 { - invID := binary.BigEndian.Uint64(k[8:16]) - ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) - } - return true - }) - - return stackitem.NewArray(ids) -} - -func (c *Collocatio) getInvestmentsByInvestor(ic *interop.Context, args []stackitem.Item) stackitem.Item { - investor := toUint160(args[0]) - - vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) - if err != nil { - return stackitem.NewArray(nil) - } - - prefix := []byte{collocatioPrefixInvestmentByInvestor} - prefix = append(prefix, make([]byte, 8)...) - binary.BigEndian.PutUint64(prefix[1:], vita.TokenID) - - var ids []stackitem.Item - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 16 { - invID := binary.BigEndian.Uint64(k[8:16]) - ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) - } - return true - }) - - return stackitem.NewArray(ids) -} - -func (c *Collocatio) getOpportunitiesByType(ic *interop.Context, args []stackitem.Item) stackitem.Item { - oppType := state.OpportunityType(toUint64(args[0])) - - prefix := []byte{collocatioPrefixOpportunityByType, byte(oppType)} - - var ids []stackitem.Item - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 9 { - oppID := binary.BigEndian.Uint64(k[1:9]) - ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) - } - return true - }) - - return stackitem.NewArray(ids) -} - -func (c *Collocatio) getOpportunitiesByStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { - status := state.OpportunityStatus(toUint64(args[0])) - - prefix := []byte{collocatioPrefixOpportunityByStatus, byte(status)} - - var ids []stackitem.Item - ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 9 { - oppID := binary.BigEndian.Uint64(k[1:9]) - ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) - } - return true - }) - - return stackitem.NewArray(ids) -} - -// ============================================================================ -// Stack Item Converters -// ============================================================================ - -func opportunityToStackItem(opp *state.InvestmentOpportunity) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Type))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Status))), - stackitem.NewByteArray(opp.Creator.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.SponsorVitaID)), - stackitem.NewByteArray([]byte(opp.Name)), - stackitem.NewByteArray([]byte(opp.Description)), - stackitem.NewByteArray(opp.TermsHash.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.CurrentParticipants)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinInvestment)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxInvestment)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TotalPool)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TargetPool)), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ExpectedReturns)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.RiskLevel))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.VotingDeadline))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.InvestmentDeadline))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.MaturityDate))), - stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ProposalID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.CreatedAt))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.UpdatedAt))), - }) -} - -func investmentToStackItem(inv *state.Investment) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(inv.OpportunityID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(inv.VitaID)), - stackitem.NewByteArray(inv.Investor.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(inv.Amount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.Status))), - stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ReturnAmount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.CreatedAt))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.UpdatedAt))), - }) -} - -func eligibilityToStackItem(elig *state.InvestorEligibility) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(elig.VitaID)), - stackitem.NewByteArray(elig.Investor.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.Eligibility))), - stackitem.NewBool(elig.ScireCompleted), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.RiskScore))), - stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalInvested)), - stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalReturns)), - stackitem.NewBigInteger(new(big.Int).SetUint64(elig.ActiveInvestments)), - stackitem.NewBigInteger(new(big.Int).SetUint64(elig.CompletedInvestments)), - stackitem.NewBool(elig.HasViolations), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.ViolationCount))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.LastActivity))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.CreatedAt))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.UpdatedAt))), - }) -} - -func violationToStackItem(v *state.InvestmentViolation) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(v.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(v.VitaID)), - stackitem.NewByteArray(v.Violator.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(v.OpportunityID)), - stackitem.NewByteArray([]byte(v.ViolationType)), - stackitem.NewByteArray([]byte(v.Description)), - stackitem.NewByteArray(v.EvidenceHash.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(v.Penalty)), - stackitem.NewByteArray(v.ReportedBy.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ReportedAt))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ResolvedAt))), - stackitem.NewByteArray([]byte(v.Resolution)), - }) -} - -func employmentToStackItem(ev *state.EmploymentVerification) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(ev.VitaID)), - stackitem.NewByteArray(ev.Employee.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(ev.EmployerVitaID)), - stackitem.NewByteArray(ev.Employer.BytesBE()), - stackitem.NewByteArray([]byte(ev.Position)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.StartDate))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.EndDate))), - stackitem.NewBool(ev.IsActive), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.VerifiedAt))), - stackitem.NewByteArray(ev.VerifiedBy.BytesBE()), - }) -} - -func contractorToStackItem(cv *state.ContractorVerification) stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(cv.VitaID)), - stackitem.NewByteArray(cv.Contractor.BytesBE()), - stackitem.NewByteArray([]byte(cv.PlatformID)), - stackitem.NewByteArray(cv.Platform.BytesBE()), - stackitem.NewByteArray([]byte(cv.ContractorID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.StartDate))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.EndDate))), - stackitem.NewBool(cv.IsActive), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.VerifiedAt))), - stackitem.NewByteArray(cv.VerifiedBy.BytesBE()), - }) -} +package native + +import ( + "encoding/binary" + "fmt" + "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/nativehashes" + "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/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" +) + +// Collocatio represents the Investment native contract for democratic investment (PIO/EIO/CIO). +// Latin: "collocatio" = placement, arrangement (investment) +type Collocatio struct { + interop.ContractMD + Tutus ITutus + Vita IVita + RoleRegistry *RoleRegistry + VTS *VTS + Scire *Scire + Eligere *Eligere + Tribute *Tribute +} + +// Storage prefixes for Collocatio contract. +const ( + collocatioPrefixConfig byte = 0x01 // -> CollocatioConfig + collocatioPrefixOpportunity byte = 0x10 // opportunityID -> InvestmentOpportunity + collocatioPrefixOpportunityByType byte = 0x11 // type + opportunityID -> exists + collocatioPrefixOpportunityByStatus byte = 0x12 // status + opportunityID -> exists + collocatioPrefixOppCounter byte = 0x1F // -> next opportunity ID + collocatioPrefixInvestment byte = 0x20 // investmentID -> Investment + collocatioPrefixInvestmentByOpp byte = 0x21 // opportunityID + investmentID -> exists + collocatioPrefixInvestmentByInvestor byte = 0x22 // vitaID + investmentID -> exists + collocatioPrefixInvCounter byte = 0x2F // -> next investment ID + collocatioPrefixEligibility byte = 0x30 // vitaID -> InvestorEligibility + collocatioPrefixEligibilityByOwner byte = 0x31 // owner -> vitaID + collocatioPrefixViolation byte = 0x40 // violationID -> InvestmentViolation + collocatioPrefixViolationByInvestor byte = 0x41 // vitaID + violationID -> exists + collocatioPrefixViolationCounter byte = 0x4F // -> next violation ID + collocatioPrefixEmployment byte = 0x50 // employeeVitaID -> EmploymentVerification + collocatioPrefixEmploymentByEmployer byte = 0x51 // employerVitaID + employeeVitaID -> exists + collocatioPrefixContractor byte = 0x60 // contractorVitaID -> ContractorVerification + collocatioPrefixContractorByPlatform byte = 0x61 // platformHash + contractorVitaID -> exists +) + +// Collocatio events. +const ( + collocatioOpportunityCreatedEvent = "OpportunityCreated" + collocatioOpportunityActivatedEvent = "OpportunityActivated" + collocatioOpportunityClosedEvent = "OpportunityClosed" + collocatioOpportunityFailedEvent = "OpportunityFailed" + collocatioOpportunityCancelledEvent = "OpportunityCancelled" + collocatioInvestmentMadeEvent = "InvestmentMade" + collocatioInvestmentWithdrawnEvent = "InvestmentWithdrawn" + collocatioReturnsDistributedEvent = "ReturnsDistributed" + collocatioEligibilityUpdatedEvent = "EligibilityUpdated" + collocatioEducationCompletedEvent = "EducationCompleted" + collocatioViolationRecordedEvent = "ViolationRecorded" + collocatioViolationResolvedEvent = "ViolationResolved" + collocatioEmploymentVerifiedEvent = "EmploymentVerified" + collocatioEmploymentRevokedEvent = "EmploymentRevoked" + collocatioContractorVerifiedEvent = "ContractorVerified" + collocatioContractorRevokedEvent = "ContractorRevoked" +) + +// RoleInvestmentManager is the role ID for investment management. +const RoleInvestmentManager uint64 = 28 + +// Default config values. +const ( + defaultMinPIOParticipants uint64 = 100 + defaultMinEIOParticipants uint64 = 10 + defaultMinCIOParticipants uint64 = 25 + defaultMinInvestment uint64 = 100_00000000 // 100 VTS + defaultMaxIndividualCap uint64 = 1_000_000_00000000 // 1M VTS + defaultWealthConcentration uint64 = 500 // 5% + defaultCreationFee uint64 = 1000_00000000 // 1000 VTS + defaultInvestmentFee uint64 = 50 // 0.5% + defaultWithdrawalPenalty uint64 = 200 // 2% + defaultMinVotingPeriod uint32 = 10000 + defaultMinInvestmentPeriod uint32 = 20000 + defaultMinMaturityPeriod uint32 = 50000 + defaultMaxViolationsBeforeBan uint8 = 3 +) + +var _ interop.Contract = (*Collocatio)(nil) + +func newCollocatio() *Collocatio { + c := &Collocatio{ + ContractMD: *interop.NewContractMD(nativenames.Collocatio, nativeids.Collocatio), + } + defer c.BuildHFSpecificMD(c.ActiveIn()) + + // getConfig + desc := NewDescriptor("getConfig", smartcontract.ArrayType) + md := NewMethodAndPrice(c.getConfig, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunityCount + desc = NewDescriptor("getOpportunityCount", smartcontract.IntegerType) + md = NewMethodAndPrice(c.getOpportunityCount, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunity + desc = NewDescriptor("getOpportunity", smartcontract.ArrayType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getOpportunity, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // createOpportunity + desc = NewDescriptor("createOpportunity", smartcontract.IntegerType, + manifest.NewParameter("oppType", smartcontract.IntegerType), + manifest.NewParameter("name", smartcontract.StringType), + manifest.NewParameter("description", smartcontract.StringType), + manifest.NewParameter("termsHash", smartcontract.Hash256Type), + manifest.NewParameter("minParticipants", smartcontract.IntegerType), + manifest.NewParameter("maxParticipants", smartcontract.IntegerType), + manifest.NewParameter("minInvestment", smartcontract.IntegerType), + manifest.NewParameter("maxInvestment", smartcontract.IntegerType), + manifest.NewParameter("targetPool", smartcontract.IntegerType), + manifest.NewParameter("expectedReturns", smartcontract.IntegerType), + manifest.NewParameter("riskLevel", smartcontract.IntegerType), + manifest.NewParameter("maturityBlocks", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.createOpportunity, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // activateOpportunity + desc = NewDescriptor("activateOpportunity", smartcontract.BoolType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.activateOpportunity, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // invest + desc = NewDescriptor("invest", smartcontract.IntegerType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.invest, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // withdraw + desc = NewDescriptor("withdraw", smartcontract.BoolType, + manifest.NewParameter("investmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.withdraw, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getEligibility + desc = NewDescriptor("getEligibility", smartcontract.ArrayType, + manifest.NewParameter("investor", smartcontract.Hash160Type)) + md = NewMethodAndPrice(c.getEligibility, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // setEligibility + desc = NewDescriptor("setEligibility", smartcontract.BoolType, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("eligibilityFlags", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.setEligibility, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // isEligible + desc = NewDescriptor("isEligible", smartcontract.BoolType, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("oppType", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.isEligible, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getInvestment + desc = NewDescriptor("getInvestment", smartcontract.ArrayType, + manifest.NewParameter("investmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getInvestment, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // getInvestmentCount + desc = NewDescriptor("getInvestmentCount", smartcontract.IntegerType) + md = NewMethodAndPrice(c.getInvestmentCount, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // recordViolation + desc = NewDescriptor("recordViolation", smartcontract.IntegerType, + manifest.NewParameter("violator", smartcontract.Hash160Type), + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("violationType", smartcontract.StringType), + manifest.NewParameter("description", smartcontract.StringType), + manifest.NewParameter("evidenceHash", smartcontract.Hash256Type), + manifest.NewParameter("penalty", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.recordViolation, 1<<17, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // resolveViolation + desc = NewDescriptor("resolveViolation", smartcontract.BoolType, + manifest.NewParameter("violationID", smartcontract.IntegerType), + manifest.NewParameter("resolution", smartcontract.StringType)) + md = NewMethodAndPrice(c.resolveViolation, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getViolation + desc = NewDescriptor("getViolation", smartcontract.ArrayType, + manifest.NewParameter("violationID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getViolation, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // verifyEmployment + desc = NewDescriptor("verifyEmployment", smartcontract.BoolType, + manifest.NewParameter("employee", smartcontract.Hash160Type), + manifest.NewParameter("employer", smartcontract.Hash160Type), + manifest.NewParameter("position", smartcontract.StringType), + manifest.NewParameter("startDate", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.verifyEmployment, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // revokeEmployment + desc = NewDescriptor("revokeEmployment", smartcontract.BoolType, + manifest.NewParameter("employee", smartcontract.Hash160Type), + manifest.NewParameter("employer", smartcontract.Hash160Type), + manifest.NewParameter("endDate", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.revokeEmployment, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getEmploymentStatus + desc = NewDescriptor("getEmploymentStatus", smartcontract.ArrayType, + manifest.NewParameter("employee", smartcontract.Hash160Type)) + md = NewMethodAndPrice(c.getEmploymentStatus, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // verifyContractor + desc = NewDescriptor("verifyContractor", smartcontract.BoolType, + manifest.NewParameter("contractor", smartcontract.Hash160Type), + manifest.NewParameter("platform", smartcontract.Hash160Type), + manifest.NewParameter("platformID", smartcontract.StringType), + manifest.NewParameter("contractorID", smartcontract.StringType), + manifest.NewParameter("startDate", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.verifyContractor, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // revokeContractor + desc = NewDescriptor("revokeContractor", smartcontract.BoolType, + manifest.NewParameter("contractor", smartcontract.Hash160Type), + manifest.NewParameter("platform", smartcontract.Hash160Type), + manifest.NewParameter("endDate", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.revokeContractor, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getContractorStatus + desc = NewDescriptor("getContractorStatus", smartcontract.ArrayType, + manifest.NewParameter("contractor", smartcontract.Hash160Type)) + md = NewMethodAndPrice(c.getContractorStatus, 1<<15, callflag.ReadStates) + c.AddMethod(md, desc) + + // completeInvestmentEducation + desc = NewDescriptor("completeInvestmentEducation", smartcontract.BoolType, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("certificationID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.completeInvestmentEducation, 1<<16, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // distributeReturns + desc = NewDescriptor("distributeReturns", smartcontract.BoolType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("actualReturns", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.distributeReturns, 1<<18, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // cancelOpportunity + desc = NewDescriptor("cancelOpportunity", smartcontract.BoolType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.cancelOpportunity, 1<<18, callflag.States|callflag.AllowNotify) + c.AddMethod(md, desc) + + // getInvestmentsByOpportunity + desc = NewDescriptor("getInvestmentsByOpportunity", smartcontract.ArrayType, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getInvestmentsByOpportunity, 1<<16, callflag.ReadStates) + c.AddMethod(md, desc) + + // getInvestmentsByInvestor + desc = NewDescriptor("getInvestmentsByInvestor", smartcontract.ArrayType, + manifest.NewParameter("investor", smartcontract.Hash160Type)) + md = NewMethodAndPrice(c.getInvestmentsByInvestor, 1<<16, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunitiesByType + desc = NewDescriptor("getOpportunitiesByType", smartcontract.ArrayType, + manifest.NewParameter("oppType", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getOpportunitiesByType, 1<<16, callflag.ReadStates) + c.AddMethod(md, desc) + + // getOpportunitiesByStatus + desc = NewDescriptor("getOpportunitiesByStatus", smartcontract.ArrayType, + manifest.NewParameter("status", smartcontract.IntegerType)) + md = NewMethodAndPrice(c.getOpportunitiesByStatus, 1<<16, callflag.ReadStates) + c.AddMethod(md, desc) + + // ===== Events ===== + eDesc := NewEventDescriptor(collocatioOpportunityCreatedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("oppType", smartcontract.IntegerType), + manifest.NewParameter("creator", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioOpportunityActivatedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioInvestmentMadeEvent, + manifest.NewParameter("investmentID", smartcontract.IntegerType), + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioInvestmentWithdrawnEvent, + manifest.NewParameter("investmentID", smartcontract.IntegerType), + manifest.NewParameter("returnAmount", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioEligibilityUpdatedEvent, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("eligibility", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioOpportunityClosedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioOpportunityFailedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioOpportunityCancelledEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioReturnsDistributedEvent, + manifest.NewParameter("opportunityID", smartcontract.IntegerType), + manifest.NewParameter("totalReturns", smartcontract.IntegerType), + manifest.NewParameter("investorCount", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioEducationCompletedEvent, + manifest.NewParameter("investor", smartcontract.Hash160Type), + manifest.NewParameter("certificationID", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioViolationRecordedEvent, + manifest.NewParameter("violationID", smartcontract.IntegerType), + manifest.NewParameter("violator", smartcontract.Hash160Type), + manifest.NewParameter("penalty", smartcontract.IntegerType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioViolationResolvedEvent, + manifest.NewParameter("violationID", smartcontract.IntegerType), + manifest.NewParameter("resolution", smartcontract.StringType)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioEmploymentVerifiedEvent, + manifest.NewParameter("employee", smartcontract.Hash160Type), + manifest.NewParameter("employer", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioEmploymentRevokedEvent, + manifest.NewParameter("employee", smartcontract.Hash160Type), + manifest.NewParameter("employer", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioContractorVerifiedEvent, + manifest.NewParameter("contractor", smartcontract.Hash160Type), + manifest.NewParameter("platform", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(collocatioContractorRevokedEvent, + manifest.NewParameter("contractor", smartcontract.Hash160Type), + manifest.NewParameter("platform", smartcontract.Hash160Type)) + c.AddEvent(NewEvent(eDesc)) + + return c +} + +// Metadata returns contract metadata. +func (c *Collocatio) Metadata() *interop.ContractMD { + return &c.ContractMD +} + +// Initialize initializes Collocatio contract. +func (c *Collocatio) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != c.ActiveIn() { + return nil + } + + // Initialize default config + cfg := state.CollocatioConfig{ + MinPIOParticipants: defaultMinPIOParticipants, + MinEIOParticipants: defaultMinEIOParticipants, + MinCIOParticipants: defaultMinCIOParticipants, + DefaultMinInvestment: defaultMinInvestment, + MaxIndividualCap: defaultMaxIndividualCap, + WealthConcentration: defaultWealthConcentration, + CreationFee: defaultCreationFee, + InvestmentFee: defaultInvestmentFee, + WithdrawalPenalty: defaultWithdrawalPenalty, + MinVotingPeriod: defaultMinVotingPeriod, + MinInvestmentPeriod: defaultMinInvestmentPeriod, + MinMaturityPeriod: defaultMinMaturityPeriod, + MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, + ViolationCooldown: 1000000, + } + c.setConfigInternal(ic.DAO, &cfg) + + return nil +} + +// InitializeCache fills native Collocatio cache from DAO. +func (c *Collocatio) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + return nil +} + +// OnPersist implements the Contract interface. +func (c *Collocatio) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the Contract interface. +// Handles lifecycle automation for opportunities. +func (c *Collocatio) PostPersist(ic *interop.Context) error { + // Run every 100 blocks for performance + if ic.Block.Index%100 != 0 { + return nil + } + + // Process opportunities that need status updates + c.processActiveOpportunities(ic) + c.processClosedOpportunities(ic) + return nil +} + +// processActiveOpportunities handles Active opportunities past their investment deadline. +func (c *Collocatio) processActiveOpportunities(ic *interop.Context) { + prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityActive)} + cfg := c.getConfigInternal(ic.DAO) + + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 9 { + return true + } + + oppID := binary.BigEndian.Uint64(k[1:9]) + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + return true + } + + // Check if investment deadline passed + if ic.Block.Index < opp.InvestmentDeadline { + return true + } + + // Get minimum participants for this opportunity type + var minParticipants uint64 + switch opp.Type { + case state.OpportunityPIO: + minParticipants = cfg.MinPIOParticipants + case state.OpportunityEIO: + minParticipants = cfg.MinEIOParticipants + case state.OpportunityCIO: + minParticipants = cfg.MinCIOParticipants + default: + minParticipants = 1 + } + + // Use opportunity's own min if set + if opp.MinParticipants > minParticipants { + minParticipants = opp.MinParticipants + } + + // Check if opportunity met minimum participants + if opp.CurrentParticipants < minParticipants { + // Failed - didn't meet minimum participants + c.updateOpportunityStatus(ic, opp, state.OpportunityFailed) + ic.AddNotification(c.Hash, collocatioOpportunityFailedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + stackitem.NewByteArray([]byte("insufficient participants")), + })) + } else { + // Success - close and move to maturity phase + c.updateOpportunityStatus(ic, opp, state.OpportunityClosed) + ic.AddNotification(c.Hash, collocatioOpportunityClosedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + })) + } + return true + }) +} + +// processClosedOpportunities handles Closed opportunities past their maturity date. +func (c *Collocatio) processClosedOpportunities(ic *interop.Context) { + prefix := []byte{collocatioPrefixOpportunityByStatus, byte(state.OpportunityClosed)} + + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) < 9 { + return true + } + + oppID := binary.BigEndian.Uint64(k[1:9]) + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + return true + } + + // Check if maturity date passed + if ic.Block.Index < opp.MaturityDate { + return true + } + + // Opportunity is mature and ready for returns distribution + // Note: Actual distribution is triggered by distributeReturns call + // This could emit a notification for off-chain systems + // For now, we just log that it's ready (no state change needed) + return true + }) +} + +// updateOpportunityStatus updates an opportunity's status and maintains status index. +func (c *Collocatio) updateOpportunityStatus(ic *interop.Context, opp *state.InvestmentOpportunity, newStatus state.OpportunityStatus) { + oldStatus := opp.Status + + // Remove from old status index + oldStatusKey := makeCollocatioOppByStatusKey(oldStatus, opp.ID) + ic.DAO.DeleteStorageItem(c.ID, oldStatusKey) + + // Update status + opp.Status = newStatus + opp.UpdatedAt = ic.Block.Index + + // Add to new status index + newStatusKey := makeCollocatioOppByStatusKey(newStatus, opp.ID) + ic.DAO.PutStorageItem(c.ID, newStatusKey, []byte{1}) + + // Save opportunity + c.putOpportunity(ic.DAO, opp) +} + +// ActiveIn returns nil (always active from genesis). +func (c *Collocatio) ActiveIn() *config.Hardfork { + return nil +} + +// ============================================================================ +// Storage Key Helpers +// ============================================================================ + +func makeCollocatioConfigKey() []byte { + return []byte{collocatioPrefixConfig} +} + +func makeCollocatioOppKey(oppID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixOpportunity + binary.BigEndian.PutUint64(key[1:], oppID) + return key +} + +func makeCollocatioOppByTypeKey(oppType state.OpportunityType, oppID uint64) []byte { + key := make([]byte, 10) + key[0] = collocatioPrefixOpportunityByType + key[1] = byte(oppType) + binary.BigEndian.PutUint64(key[2:], oppID) + return key +} + +func makeCollocatioOppByStatusKey(status state.OpportunityStatus, oppID uint64) []byte { + key := make([]byte, 10) + key[0] = collocatioPrefixOpportunityByStatus + key[1] = byte(status) + binary.BigEndian.PutUint64(key[2:], oppID) + return key +} + +func makeCollocatioOppCounterKey() []byte { + return []byte{collocatioPrefixOppCounter} +} + +func makeCollocatioInvKey(invID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixInvestment + binary.BigEndian.PutUint64(key[1:], invID) + return key +} + +func makeCollocatioInvByOppKey(oppID, invID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixInvestmentByOpp + binary.BigEndian.PutUint64(key[1:9], oppID) + binary.BigEndian.PutUint64(key[9:], invID) + return key +} + +func makeCollocatioInvByInvestorKey(vitaID, invID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixInvestmentByInvestor + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], invID) + return key +} + +func makeCollocatioInvCounterKey() []byte { + return []byte{collocatioPrefixInvCounter} +} + +func makeCollocatioEligKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixEligibility + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func makeCollocatioEligByOwnerKey(owner util.Uint160) []byte { + key := make([]byte, 1+util.Uint160Size) + key[0] = collocatioPrefixEligibilityByOwner + copy(key[1:], owner.BytesBE()) + return key +} + +func makeCollocatioViolationKey(violationID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixViolation + binary.BigEndian.PutUint64(key[1:], violationID) + return key +} + +func makeCollocatioViolationByInvestorKey(vitaID, violationID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixViolationByInvestor + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], violationID) + return key +} + +func makeCollocatioViolationCounterKey() []byte { + return []byte{collocatioPrefixViolationCounter} +} + +func makeCollocatioEmploymentKey(employeeVitaID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixEmployment + binary.BigEndian.PutUint64(key[1:], employeeVitaID) + return key +} + +func makeCollocatioEmploymentByEmployerKey(employerVitaID, employeeVitaID uint64) []byte { + key := make([]byte, 17) + key[0] = collocatioPrefixEmploymentByEmployer + binary.BigEndian.PutUint64(key[1:9], employerVitaID) + binary.BigEndian.PutUint64(key[9:], employeeVitaID) + return key +} + +func makeCollocatioContractorKey(contractorVitaID uint64) []byte { + key := make([]byte, 9) + key[0] = collocatioPrefixContractor + binary.BigEndian.PutUint64(key[1:], contractorVitaID) + return key +} + +func makeCollocatioContractorByPlatformKey(platform util.Uint160, contractorVitaID uint64) []byte { + key := make([]byte, 1+util.Uint160Size+8) + key[0] = collocatioPrefixContractorByPlatform + copy(key[1:1+util.Uint160Size], platform.BytesBE()) + binary.BigEndian.PutUint64(key[1+util.Uint160Size:], contractorVitaID) + return key +} + +// ============================================================================ +// Internal Storage Methods +// ============================================================================ + +func (c *Collocatio) getConfigInternal(d *dao.Simple) *state.CollocatioConfig { + si := d.GetStorageItem(c.ID, makeCollocatioConfigKey()) + if si == nil { + return &state.CollocatioConfig{ + MinPIOParticipants: defaultMinPIOParticipants, + MinEIOParticipants: defaultMinEIOParticipants, + MinCIOParticipants: defaultMinCIOParticipants, + DefaultMinInvestment: defaultMinInvestment, + MaxIndividualCap: defaultMaxIndividualCap, + WealthConcentration: defaultWealthConcentration, + CreationFee: defaultCreationFee, + InvestmentFee: defaultInvestmentFee, + WithdrawalPenalty: defaultWithdrawalPenalty, + MinVotingPeriod: defaultMinVotingPeriod, + MinInvestmentPeriod: defaultMinInvestmentPeriod, + MinMaturityPeriod: defaultMinMaturityPeriod, + MaxViolationsBeforeBan: defaultMaxViolationsBeforeBan, + ViolationCooldown: 1000000, + } + } + cfg := new(state.CollocatioConfig) + item, _ := stackitem.Deserialize(si) + cfg.FromStackItem(item) + return cfg +} + +func (c *Collocatio) setConfigInternal(d *dao.Simple, cfg *state.CollocatioConfig) { + item, _ := cfg.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioConfigKey(), data) +} + +func (c *Collocatio) getCounter(d *dao.Simple, key []byte) uint64 { + si := d.GetStorageItem(c.ID, key) + if si == nil || len(si) < 8 { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (c *Collocatio) incrementCounter(d *dao.Simple, key []byte) uint64 { + current := c.getCounter(d, key) + next := current + 1 + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, next) + d.PutStorageItem(c.ID, key, buf) + return next +} + +func (c *Collocatio) getOpportunityInternal(d *dao.Simple, oppID uint64) *state.InvestmentOpportunity { + si := d.GetStorageItem(c.ID, makeCollocatioOppKey(oppID)) + if si == nil { + return nil + } + opp := new(state.InvestmentOpportunity) + item, _ := stackitem.Deserialize(si) + opp.FromStackItem(item) + return opp +} + +func (c *Collocatio) putOpportunity(d *dao.Simple, opp *state.InvestmentOpportunity) { + item, _ := opp.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioOppKey(opp.ID), data) +} + +func (c *Collocatio) getInvestmentInternal(d *dao.Simple, invID uint64) *state.Investment { + si := d.GetStorageItem(c.ID, makeCollocatioInvKey(invID)) + if si == nil { + return nil + } + inv := new(state.Investment) + item, _ := stackitem.Deserialize(si) + inv.FromStackItem(item) + return inv +} + +func (c *Collocatio) putInvestment(d *dao.Simple, inv *state.Investment) { + item, _ := inv.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioInvKey(inv.ID), data) +} + +func (c *Collocatio) getEligibilityInternal(d *dao.Simple, investor util.Uint160) *state.InvestorEligibility { + // First get vitaID from owner mapping + si := d.GetStorageItem(c.ID, makeCollocatioEligByOwnerKey(investor)) + if si == nil || len(si) < 8 { + return nil + } + vitaID := binary.BigEndian.Uint64(si) + + // Then get eligibility + eligSI := d.GetStorageItem(c.ID, makeCollocatioEligKey(vitaID)) + if eligSI == nil { + return nil + } + elig := new(state.InvestorEligibility) + item, _ := stackitem.Deserialize(eligSI) + elig.FromStackItem(item) + return elig +} + +func (c *Collocatio) putEligibility(d *dao.Simple, elig *state.InvestorEligibility) { + item, _ := elig.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioEligKey(elig.VitaID), data) + + // Also store owner -> vitaID mapping + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, elig.VitaID) + d.PutStorageItem(c.ID, makeCollocatioEligByOwnerKey(elig.Investor), buf) +} + +// ============================================================================ +// Contract Methods +// ============================================================================ + +func (c *Collocatio) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + cfg := c.getConfigInternal(ic.DAO) + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinPIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinEIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MinCIOParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.DefaultMinInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxIndividualCap)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WealthConcentration)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.CreationFee)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.InvestmentFee)), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.WithdrawalPenalty)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinVotingPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinInvestmentPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MinMaturityPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.MaxViolationsBeforeBan))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cfg.ViolationCooldown))), + }) +} + +func (c *Collocatio) getOpportunityCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := c.getCounter(ic.DAO, makeCollocatioOppCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +func (c *Collocatio) getOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + return stackitem.Null{} + } + return opportunityToStackItem(opp) +} + +func (c *Collocatio) createOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + caller := ic.VM.GetCallingScriptHash() + + oppType := state.OpportunityType(toUint64(args[0])) + name := toString(args[1]) + description := toString(args[2]) + termsHashBytes, err := args[3].TryBytes() + if err != nil { + panic(err) + } + termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes) + if err != nil { + panic(err) + } + minParticipants := toUint64(args[4]) + maxParticipants := toUint64(args[5]) + minInvestment := toUint64(args[6]) + maxInvestment := toUint64(args[7]) + targetPool := toUint64(args[8]) + expectedReturns := toUint64(args[9]) + riskLevel := uint8(toUint64(args[10])) + maturityBlocks := uint32(toUint64(args[11])) + + // Validate caller has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil { + panic("caller must have Vita token") + } + vitaID := vita.TokenID + + // Validate opportunity type + if oppType > state.OpportunityCIO { + panic("invalid opportunity type") + } + + // Validate parameters + cfg := c.getConfigInternal(ic.DAO) + if maturityBlocks < cfg.MinMaturityPeriod { + panic("maturity period too short") + } + if riskLevel < 1 || riskLevel > 10 { + panic("risk level must be 1-10") + } + + // Get minimum participants based on type + var minRequired uint64 + switch oppType { + case state.OpportunityPIO: + minRequired = cfg.MinPIOParticipants + case state.OpportunityEIO: + minRequired = cfg.MinEIOParticipants + case state.OpportunityCIO: + minRequired = cfg.MinCIOParticipants + } + if minParticipants < minRequired { + panic(fmt.Sprintf("minimum participants must be at least %d for this type", minRequired)) + } + + // Charge creation fee to Treasury + if cfg.CreationFee > 0 { + if err := c.VTS.transferUnrestricted(ic, caller, nativehashes.Treasury, new(big.Int).SetUint64(cfg.CreationFee), nil); err != nil { + panic("failed to pay creation fee") + } + } + + // Create opportunity + oppID := c.incrementCounter(ic.DAO, makeCollocatioOppCounterKey()) + currentBlock := ic.Block.Index + + opp := &state.InvestmentOpportunity{ + ID: oppID, + Type: oppType, + Status: state.OpportunityDraft, + Creator: caller, + SponsorVitaID: vitaID, + Name: name, + Description: description, + TermsHash: termsHash, + MinParticipants: minParticipants, + MaxParticipants: maxParticipants, + CurrentParticipants: 0, + MinInvestment: minInvestment, + MaxInvestment: maxInvestment, + TotalPool: 0, + TargetPool: targetPool, + ExpectedReturns: expectedReturns, + RiskLevel: riskLevel, + VotingDeadline: currentBlock + cfg.MinVotingPeriod, + InvestmentDeadline: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod, + MaturityDate: currentBlock + cfg.MinVotingPeriod + cfg.MinInvestmentPeriod + maturityBlocks, + ProposalID: 0, + CreatedAt: currentBlock, + UpdatedAt: currentBlock, + } + + c.putOpportunity(ic.DAO, opp) + + // Store type index + ic.DAO.PutStorageItem(c.ID, makeCollocatioOppByTypeKey(oppType, oppID), []byte{1}) + + // Emit event + ic.AddNotification(c.Hash, collocatioOpportunityCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(oppType))), + stackitem.NewByteArray(caller.BytesBE()), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)) +} + +func (c *Collocatio) activateOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + + caller := ic.VM.GetCallingScriptHash() + if caller != opp.Creator && !c.Tutus.CheckCommittee(ic) { + panic("only creator or committee can activate") + } + + if opp.Status != state.OpportunityDraft { + panic("opportunity must be in draft status") + } + + opp.Status = state.OpportunityActive + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + ic.AddNotification(c.Hash, collocatioOpportunityActivatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) invest(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + amount := toUint64(args[1]) + caller := ic.VM.GetCallingScriptHash() + + // Validate caller has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil { + panic("caller must have Vita token") + } + vitaID := vita.TokenID + + // Get opportunity + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + + if opp.Status != state.OpportunityActive { + panic("opportunity is not active") + } + if ic.Block.Index > opp.InvestmentDeadline { + panic("investment deadline has passed") + } + + if opp.MaxParticipants > 0 && opp.CurrentParticipants >= opp.MaxParticipants { + panic("maximum participants reached") + } + + if amount < opp.MinInvestment { + panic("investment below minimum") + } + if amount > opp.MaxInvestment { + panic("investment exceeds maximum") + } + + // Check eligibility + if !c.isEligibleInternal(ic.DAO, caller, opp.Type) { + panic("investor not eligible for this opportunity type") + } + + // Calculate fee + cfg := c.getConfigInternal(ic.DAO) + fee := (amount * cfg.InvestmentFee) / 10000 + netAmount := amount - fee + + // Transfer VTS from investor + if err := c.VTS.transferUnrestricted(ic, caller, c.Hash, new(big.Int).SetUint64(amount), nil); err != nil { + panic("failed to transfer investment amount") + } + + // Send fee to Treasury + if fee > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(fee), nil); err != nil { + panic("failed to transfer fee to treasury") + } + } + + // Create investment record + invID := c.incrementCounter(ic.DAO, makeCollocatioInvCounterKey()) + + inv := &state.Investment{ + ID: invID, + OpportunityID: oppID, + VitaID: vitaID, + Investor: caller, + Amount: netAmount, + Status: state.InvestmentActive, + ReturnAmount: 0, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + + c.putInvestment(ic.DAO, inv) + + // Store indexes + ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByOppKey(oppID, invID), []byte{1}) + ic.DAO.PutStorageItem(c.ID, makeCollocatioInvByInvestorKey(vitaID, invID), []byte{1}) + + // Update opportunity + opp.CurrentParticipants++ + opp.TotalPool += netAmount + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + // Update eligibility stats + c.updateEligibilityOnInvest(ic.DAO, caller, vitaID, netAmount, ic.Block.Index) + + // Emit event + ic.AddNotification(c.Hash, collocatioInvestmentMadeEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + stackitem.NewByteArray(caller.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(netAmount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(invID)) +} + +func (c *Collocatio) withdraw(ic *interop.Context, args []stackitem.Item) stackitem.Item { + invID := toUint64(args[0]) + caller := ic.VM.GetCallingScriptHash() + + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil { + panic("investment not found") + } + + if inv.Investor != caller { + panic("only investor can withdraw") + } + + if inv.Status != state.InvestmentActive { + panic("investment is not active") + } + + opp := c.getOpportunityInternal(ic.DAO, inv.OpportunityID) + if opp == nil { + panic("opportunity not found") + } + + // Calculate penalty for early withdrawal + cfg := c.getConfigInternal(ic.DAO) + returnAmount := inv.Amount + if ic.Block.Index < opp.MaturityDate && cfg.WithdrawalPenalty > 0 { + penalty := (inv.Amount * cfg.WithdrawalPenalty) / 10000 + returnAmount = inv.Amount - penalty + if penalty > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { + panic("failed to transfer penalty") + } + } + } + + // Return funds + if err := c.VTS.transferUnrestricted(ic, c.Hash, caller, new(big.Int).SetUint64(returnAmount), nil); err != nil { + panic("failed to return investment") + } + + // Update investment + inv.Status = state.InvestmentWithdrawn + inv.UpdatedAt = ic.Block.Index + c.putInvestment(ic.DAO, inv) + + // Update opportunity + opp.CurrentParticipants-- + opp.TotalPool -= inv.Amount + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + // Update eligibility + c.updateEligibilityOnWithdraw(ic.DAO, caller, inv.Amount, ic.Block.Index) + + // Emit event + ic.AddNotification(c.Hash, collocatioInvestmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) getEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + elig := c.getEligibilityInternal(ic.DAO, investor) + if elig == nil { + return stackitem.Null{} + } + return eligibilityToStackItem(elig) +} + +func (c *Collocatio) setEligibility(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + eligFlags := state.EligibilityType(toUint64(args[1])) + + // Only committee or RoleInvestmentManager can set eligibility + if !c.Tutus.CheckCommittee(ic) { + caller := ic.VM.GetCallingScriptHash() + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can set eligibility") + } + } + + vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) + if err != nil { + panic("investor must have Vita token") + } + vitaID := vita.TokenID + + elig := c.getEligibilityInternal(ic.DAO, investor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vitaID, + Investor: investor, + CreatedAt: ic.Block.Index, + } + } + + elig.Eligibility = eligFlags + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + ic.AddNotification(c.Hash, collocatioEligibilityUpdatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(eligFlags))), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) isEligible(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + oppType := state.OpportunityType(toUint64(args[1])) + return stackitem.NewBool(c.isEligibleInternal(ic.DAO, investor, oppType)) +} + +func (c *Collocatio) isEligibleInternal(d *dao.Simple, investor util.Uint160, oppType state.OpportunityType) bool { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + return false + } + + // Must have completed investment education + if !elig.ScireCompleted { + return false + } + + // Check for ban + if elig.HasViolations { + cfg := c.getConfigInternal(d) + if elig.ViolationCount >= cfg.MaxViolationsBeforeBan { + return false + } + } + + switch oppType { + case state.OpportunityPIO: + return elig.Eligibility&state.EligibilityPIO != 0 + case state.OpportunityEIO: + if elig.Eligibility&state.EligibilityEIO == 0 { + return false + } + // Additionally verify active employment + return c.hasActiveEmployment(d, elig.VitaID) + case state.OpportunityCIO: + if elig.Eligibility&state.EligibilityCIO == 0 { + return false + } + // Additionally verify active contractor status + return c.hasActiveContractor(d, elig.VitaID) + default: + return false + } +} + +func (c *Collocatio) getInvestment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + invID := toUint64(args[0]) + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil { + return stackitem.Null{} + } + return investmentToStackItem(inv) +} + +func (c *Collocatio) getInvestmentCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := c.getCounter(ic.DAO, makeCollocatioInvCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +// ============================================================================ +// Internal Helpers +// ============================================================================ + +func (c *Collocatio) updateEligibilityOnInvest(d *dao.Simple, investor util.Uint160, vitaID, amount uint64, blockHeight uint32) { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vitaID, + Investor: investor, + CreatedAt: blockHeight, + } + } + elig.TotalInvested += amount + elig.ActiveInvestments++ + elig.LastActivity = blockHeight + elig.UpdatedAt = blockHeight + c.putEligibility(d, elig) +} + +func (c *Collocatio) updateEligibilityOnWithdraw(d *dao.Simple, investor util.Uint160, amount uint64, blockHeight uint32) { + elig := c.getEligibilityInternal(d, investor) + if elig == nil { + return + } + if elig.TotalInvested >= amount { + elig.TotalInvested -= amount + } + if elig.ActiveInvestments > 0 { + elig.ActiveInvestments-- + } + elig.LastActivity = blockHeight + elig.UpdatedAt = blockHeight + c.putEligibility(d, elig) +} + +// ============================================================================ +// Violation System +// ============================================================================ + +func (c *Collocatio) getViolationInternal(d *dao.Simple, violationID uint64) *state.InvestmentViolation { + si := d.GetStorageItem(c.ID, makeCollocatioViolationKey(violationID)) + if si == nil { + return nil + } + v := new(state.InvestmentViolation) + item, _ := stackitem.Deserialize(si) + v.FromStackItem(item) + return v +} + +func (c *Collocatio) putViolation(d *dao.Simple, v *state.InvestmentViolation) { + item, _ := v.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioViolationKey(v.ID), data) +} + +func (c *Collocatio) recordViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { + violator := toUint160(args[0]) + opportunityID := toUint64(args[1]) + violationType := toString(args[2]) + description := toString(args[3]) + evidenceHashBytes, err := args[4].TryBytes() + if err != nil { + panic(err) + } + evidenceHash, err := util.Uint256DecodeBytesBE(evidenceHashBytes) + if err != nil { + panic(err) + } + penalty := toUint64(args[5]) + + // Authorization: Committee or RoleInvestmentManager + if !c.Tutus.CheckCommittee(ic) { + caller := ic.VM.GetCallingScriptHash() + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can record violations") + } + } + + // Verify violator has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, violator) + if err != nil || vita == nil { + panic("violator must have active Vita") + } + vitaID := vita.TokenID + + // Create violation record + violationID := c.incrementCounter(ic.DAO, makeCollocatioViolationCounterKey()) + caller := ic.VM.GetCallingScriptHash() + + v := &state.InvestmentViolation{ + ID: violationID, + VitaID: vitaID, + Violator: violator, + OpportunityID: opportunityID, + ViolationType: violationType, + Description: description, + EvidenceHash: evidenceHash, + Penalty: penalty, + ReportedBy: caller, + ReportedAt: ic.Block.Index, + ResolvedAt: 0, + Resolution: "", + } + + c.putViolation(ic.DAO, v) + + // Store index by investor + ic.DAO.PutStorageItem(c.ID, makeCollocatioViolationByInvestorKey(vitaID, violationID), []byte{1}) + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, violator) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vitaID, + Investor: violator, + CreatedAt: ic.Block.Index, + } + } + elig.HasViolations = true + elig.ViolationCount++ + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + // Apply penalty if specified + if penalty > 0 { + if err := c.VTS.transferUnrestricted(ic, violator, nativehashes.Treasury, new(big.Int).SetUint64(penalty), nil); err != nil { + // Don't panic if transfer fails, just record violation without penalty + } + } + + // Emit event + ic.AddNotification(c.Hash, collocatioViolationRecordedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), + stackitem.NewByteArray(violator.BytesBE()), + stackitem.NewByteArray([]byte(violationType)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)) +} + +func (c *Collocatio) resolveViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { + violationID := toUint64(args[0]) + resolution := toString(args[1]) + + // Authorization: Committee or RoleInvestmentManager + if !c.Tutus.CheckCommittee(ic) { + caller := ic.VM.GetCallingScriptHash() + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can resolve violations") + } + } + + v := c.getViolationInternal(ic.DAO, violationID) + if v == nil { + panic("violation not found") + } + if v.ResolvedAt != 0 { + panic("violation already resolved") + } + + v.ResolvedAt = ic.Block.Index + v.Resolution = resolution + c.putViolation(ic.DAO, v) + + // Emit event + ic.AddNotification(c.Hash, collocatioViolationResolvedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(violationID)), + stackitem.NewByteArray([]byte(resolution)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) getViolation(ic *interop.Context, args []stackitem.Item) stackitem.Item { + violationID := toUint64(args[0]) + v := c.getViolationInternal(ic.DAO, violationID) + if v == nil { + return stackitem.Null{} + } + return violationToStackItem(v) +} + +// ============================================================================ +// Employment Verification (EIO) +// ============================================================================ + +func (c *Collocatio) getEmploymentInternal(d *dao.Simple, employeeVitaID uint64) *state.EmploymentVerification { + si := d.GetStorageItem(c.ID, makeCollocatioEmploymentKey(employeeVitaID)) + if si == nil { + return nil + } + ev := new(state.EmploymentVerification) + item, _ := stackitem.Deserialize(si) + ev.FromStackItem(item) + return ev +} + +func (c *Collocatio) putEmployment(d *dao.Simple, ev *state.EmploymentVerification) { + item, _ := ev.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioEmploymentKey(ev.VitaID), data) +} + +func (c *Collocatio) verifyEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + employee := toUint160(args[0]) + employer := toUint160(args[1]) + position := toString(args[2]) + startDate := uint32(toUint64(args[3])) + + caller := ic.VM.GetCallingScriptHash() + + // Authorization: Committee, RoleInvestmentManager, or employer + isAuthorized := c.Tutus.CheckCommittee(ic) || + c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || + caller == employer + + if !isAuthorized { + panic("only committee, investment manager, or employer can verify employment") + } + + // Verify both employee and employer have Vita + employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) + if err != nil { + panic("employee must have Vita token") + } + employerVita, err := c.Vita.GetTokenByOwner(ic.DAO, employer) + if err != nil { + panic("employer must have Vita token") + } + + ev := &state.EmploymentVerification{ + VitaID: employeeVita.TokenID, + Employee: employee, + EmployerVitaID: employerVita.TokenID, + Employer: employer, + Position: position, + StartDate: startDate, + EndDate: 0, + IsActive: true, + VerifiedAt: ic.Block.Index, + VerifiedBy: caller, + } + + c.putEmployment(ic.DAO, ev) + + // Store index by employer + ic.DAO.PutStorageItem(c.ID, makeCollocatioEmploymentByEmployerKey(employerVita.TokenID, employeeVita.TokenID), []byte{1}) + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, employee) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: employeeVita.TokenID, + Investor: employee, + CreatedAt: ic.Block.Index, + } + } + elig.Eligibility |= state.EligibilityEIO + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + // Emit event + ic.AddNotification(c.Hash, collocatioEmploymentVerifiedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(employee.BytesBE()), + stackitem.NewByteArray(employer.BytesBE()), + stackitem.NewByteArray([]byte(position)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) revokeEmployment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + employee := toUint160(args[0]) + employer := toUint160(args[1]) + endDate := uint32(toUint64(args[2])) + + caller := ic.VM.GetCallingScriptHash() + + // Authorization: Committee, RoleInvestmentManager, or employer + isAuthorized := c.Tutus.CheckCommittee(ic) || + c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || + caller == employer + + if !isAuthorized { + panic("only committee, investment manager, or employer can revoke employment") + } + + // Get employee Vita + employeeVita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) + if err != nil { + panic("employee must have Vita token") + } + + ev := c.getEmploymentInternal(ic.DAO, employeeVita.TokenID) + if ev == nil { + panic("employment not found") + } + if !ev.IsActive { + panic("employment already revoked") + } + if ev.Employer != employer { + panic("employer mismatch") + } + + ev.IsActive = false + ev.EndDate = endDate + c.putEmployment(ic.DAO, ev) + + // Remove EIO eligibility + elig := c.getEligibilityInternal(ic.DAO, employee) + if elig != nil { + elig.Eligibility &^= state.EligibilityEIO + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + } + + // Emit event + ic.AddNotification(c.Hash, collocatioEmploymentRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(employee.BytesBE()), + stackitem.NewByteArray(employer.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) getEmploymentStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { + employee := toUint160(args[0]) + + vita, err := c.Vita.GetTokenByOwner(ic.DAO, employee) + if err != nil { + return stackitem.Null{} + } + + ev := c.getEmploymentInternal(ic.DAO, vita.TokenID) + if ev == nil { + return stackitem.Null{} + } + return employmentToStackItem(ev) +} + +func (c *Collocatio) hasActiveEmployment(d *dao.Simple, vitaID uint64) bool { + ev := c.getEmploymentInternal(d, vitaID) + return ev != nil && ev.IsActive +} + +// ============================================================================ +// Contractor Verification (CIO) +// ============================================================================ + +func (c *Collocatio) getContractorInternal(d *dao.Simple, contractorVitaID uint64) *state.ContractorVerification { + si := d.GetStorageItem(c.ID, makeCollocatioContractorKey(contractorVitaID)) + if si == nil { + return nil + } + cv := new(state.ContractorVerification) + item, _ := stackitem.Deserialize(si) + cv.FromStackItem(item) + return cv +} + +func (c *Collocatio) putContractor(d *dao.Simple, cv *state.ContractorVerification) { + item, _ := cv.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(c.ID, makeCollocatioContractorKey(cv.VitaID), data) +} + +func (c *Collocatio) verifyContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { + contractor := toUint160(args[0]) + platform := toUint160(args[1]) + platformID := toString(args[2]) + contractorID := toString(args[3]) + startDate := uint32(toUint64(args[4])) + + caller := ic.VM.GetCallingScriptHash() + + // Authorization: Committee, RoleInvestmentManager, or platform + isAuthorized := c.Tutus.CheckCommittee(ic) || + c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || + caller == platform + + if !isAuthorized { + panic("only committee, investment manager, or platform can verify contractor") + } + + // Verify contractor has Vita + contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) + if err != nil { + panic("contractor must have Vita token") + } + + cv := &state.ContractorVerification{ + VitaID: contractorVita.TokenID, + Contractor: contractor, + PlatformID: platformID, + Platform: platform, + ContractorID: contractorID, + StartDate: startDate, + EndDate: 0, + IsActive: true, + VerifiedAt: ic.Block.Index, + VerifiedBy: caller, + } + + c.putContractor(ic.DAO, cv) + + // Store index by platform + ic.DAO.PutStorageItem(c.ID, makeCollocatioContractorByPlatformKey(platform, contractorVita.TokenID), []byte{1}) + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, contractor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: contractorVita.TokenID, + Investor: contractor, + CreatedAt: ic.Block.Index, + } + } + elig.Eligibility |= state.EligibilityCIO + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + // Emit event + ic.AddNotification(c.Hash, collocatioContractorVerifiedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(contractor.BytesBE()), + stackitem.NewByteArray(platform.BytesBE()), + stackitem.NewByteArray([]byte(platformID)), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) revokeContractor(ic *interop.Context, args []stackitem.Item) stackitem.Item { + contractor := toUint160(args[0]) + platform := toUint160(args[1]) + endDate := uint32(toUint64(args[2])) + + caller := ic.VM.GetCallingScriptHash() + + // Authorization: Committee, RoleInvestmentManager, or platform + isAuthorized := c.Tutus.CheckCommittee(ic) || + c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) || + caller == platform + + if !isAuthorized { + panic("only committee, investment manager, or platform can revoke contractor") + } + + // Get contractor Vita + contractorVita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) + if err != nil { + panic("contractor must have Vita token") + } + + cv := c.getContractorInternal(ic.DAO, contractorVita.TokenID) + if cv == nil { + panic("contractor not found") + } + if !cv.IsActive { + panic("contractor already revoked") + } + if cv.Platform != platform { + panic("platform mismatch") + } + + cv.IsActive = false + cv.EndDate = endDate + c.putContractor(ic.DAO, cv) + + // Remove CIO eligibility + elig := c.getEligibilityInternal(ic.DAO, contractor) + if elig != nil { + elig.Eligibility &^= state.EligibilityCIO + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + } + + // Emit event + ic.AddNotification(c.Hash, collocatioContractorRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(contractor.BytesBE()), + stackitem.NewByteArray(platform.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) getContractorStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { + contractor := toUint160(args[0]) + + vita, err := c.Vita.GetTokenByOwner(ic.DAO, contractor) + if err != nil { + return stackitem.Null{} + } + + cv := c.getContractorInternal(ic.DAO, vita.TokenID) + if cv == nil { + return stackitem.Null{} + } + return contractorToStackItem(cv) +} + +func (c *Collocatio) hasActiveContractor(d *dao.Simple, vitaID uint64) bool { + cv := c.getContractorInternal(d, vitaID) + return cv != nil && cv.IsActive +} + +// ============================================================================ +// Education Completion +// ============================================================================ + +func (c *Collocatio) completeInvestmentEducation(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + + caller := ic.VM.GetCallingScriptHash() + + // Authorization: Committee or RoleInvestmentManager + if !c.Tutus.CheckCommittee(ic) { + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can complete education") + } + } + + // Verify investor has Vita + vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) + if err != nil { + panic("investor must have Vita token") + } + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, investor) + if elig == nil { + elig = &state.InvestorEligibility{ + VitaID: vita.TokenID, + Investor: investor, + CreatedAt: ic.Block.Index, + } + } + elig.ScireCompleted = true + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + + // Emit event + ic.AddNotification(c.Hash, collocatioEducationCompletedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(investor.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +// ============================================================================ +// Returns Distribution & Cancellation +// ============================================================================ + +func (c *Collocatio) distributeReturns(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + actualReturns := toUint64(args[1]) + + // Authorization: Committee or RoleInvestmentManager + if !c.Tutus.CheckCommittee(ic) { + caller := ic.VM.GetCallingScriptHash() + if !c.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleInvestmentManager, ic.Block.Index) { + panic("only committee or investment manager can distribute returns") + } + } + + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + if opp.Status != state.OpportunityClosed { + panic("opportunity must be in closed status") + } + if ic.Block.Index < opp.MaturityDate { + panic("maturity date not reached") + } + if opp.TotalPool == 0 { + panic("no investments to distribute") + } + + // Collect all investment IDs first (to avoid modifying during iteration) + prefix := []byte{collocatioPrefixInvestmentByOpp} + prefix = append(prefix, make([]byte, 8)...) + binary.BigEndian.PutUint64(prefix[1:], oppID) + + var invIDs []uint64 + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 16 { + invID := binary.BigEndian.Uint64(k[8:16]) + invIDs = append(invIDs, invID) + } + return true + }) + + // Process each investment + for _, invID := range invIDs { + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil || inv.Status != state.InvestmentActive { + continue + } + + // Calculate proportional return + returnAmount := (inv.Amount * actualReturns) / opp.TotalPool + + // Transfer return to investor + if returnAmount > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(returnAmount), nil); err != nil { + continue // Skip failed transfers + } + } + + // Also return principal + if inv.Amount > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { + continue + } + } + + // Update investment + inv.ReturnAmount = returnAmount + inv.Status = state.InvestmentCompleted + inv.UpdatedAt = ic.Block.Index + c.putInvestment(ic.DAO, inv) + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, inv.Investor) + if elig != nil { + elig.TotalReturns += returnAmount + elig.CompletedInvestments++ + if elig.ActiveInvestments > 0 { + elig.ActiveInvestments-- + } + if elig.TotalInvested >= inv.Amount { + elig.TotalInvested -= inv.Amount + } + elig.LastActivity = ic.Block.Index + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + } + + // Emit event for each distribution + ic.AddNotification(c.Hash, collocatioReturnsDistributedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(invID)), + stackitem.NewByteArray(inv.Investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(returnAmount)), + })) + } + + // Update opportunity status + opp.Status = state.OpportunityCompleted + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + return stackitem.NewBool(true) +} + +func (c *Collocatio) cancelOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + caller := ic.VM.GetCallingScriptHash() + + opp := c.getOpportunityInternal(ic.DAO, oppID) + if opp == nil { + panic("opportunity not found") + } + + // Authorization: Creator (if Draft/Voting) or Committee + isCreator := caller == opp.Creator + isCommittee := c.Tutus.CheckCommittee(ic) + + if opp.Status == state.OpportunityDraft || opp.Status == state.OpportunityVoting { + if !isCreator && !isCommittee { + panic("only creator or committee can cancel draft/voting opportunity") + } + } else if opp.Status == state.OpportunityActive { + if !isCommittee { + panic("only committee can cancel active opportunity") + } + } else { + panic("opportunity cannot be cancelled in current status") + } + + // Collect all investment IDs first (to avoid modifying during iteration) + prefix := []byte{collocatioPrefixInvestmentByOpp} + prefix = append(prefix, make([]byte, 8)...) + binary.BigEndian.PutUint64(prefix[1:], oppID) + + var invIDs []uint64 + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 16 { + invID := binary.BigEndian.Uint64(k[8:16]) + invIDs = append(invIDs, invID) + } + return true + }) + + // Process each investment + for _, invID := range invIDs { + inv := c.getInvestmentInternal(ic.DAO, invID) + if inv == nil || inv.Status != state.InvestmentActive { + continue + } + + // Refund full amount + if inv.Amount > 0 { + if err := c.VTS.transferUnrestricted(ic, c.Hash, inv.Investor, new(big.Int).SetUint64(inv.Amount), nil); err != nil { + continue + } + } + + // Update investment + inv.Status = state.InvestmentRefunded + inv.UpdatedAt = ic.Block.Index + c.putInvestment(ic.DAO, inv) + + // Update eligibility + elig := c.getEligibilityInternal(ic.DAO, inv.Investor) + if elig != nil { + if elig.ActiveInvestments > 0 { + elig.ActiveInvestments-- + } + if elig.TotalInvested >= inv.Amount { + elig.TotalInvested -= inv.Amount + } + elig.LastActivity = ic.Block.Index + elig.UpdatedAt = ic.Block.Index + c.putEligibility(ic.DAO, elig) + } + } + + // Update opportunity status + opp.Status = state.OpportunityCancelled + opp.UpdatedAt = ic.Block.Index + c.putOpportunity(ic.DAO, opp) + + // Emit event + ic.AddNotification(c.Hash, collocatioOpportunityCancelledEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(oppID)), + })) + + return stackitem.NewBool(true) +} + +// ============================================================================ +// Query Methods +// ============================================================================ + +func (c *Collocatio) getInvestmentsByOpportunity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppID := toUint64(args[0]) + + prefix := []byte{collocatioPrefixInvestmentByOpp} + prefix = append(prefix, make([]byte, 8)...) + binary.BigEndian.PutUint64(prefix[1:], oppID) + + var ids []stackitem.Item + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 16 { + invID := binary.BigEndian.Uint64(k[8:16]) + ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) + } + return true + }) + + return stackitem.NewArray(ids) +} + +func (c *Collocatio) getInvestmentsByInvestor(ic *interop.Context, args []stackitem.Item) stackitem.Item { + investor := toUint160(args[0]) + + vita, err := c.Vita.GetTokenByOwner(ic.DAO, investor) + if err != nil { + return stackitem.NewArray(nil) + } + + prefix := []byte{collocatioPrefixInvestmentByInvestor} + prefix = append(prefix, make([]byte, 8)...) + binary.BigEndian.PutUint64(prefix[1:], vita.TokenID) + + var ids []stackitem.Item + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 16 { + invID := binary.BigEndian.Uint64(k[8:16]) + ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(invID))) + } + return true + }) + + return stackitem.NewArray(ids) +} + +func (c *Collocatio) getOpportunitiesByType(ic *interop.Context, args []stackitem.Item) stackitem.Item { + oppType := state.OpportunityType(toUint64(args[0])) + + prefix := []byte{collocatioPrefixOpportunityByType, byte(oppType)} + + var ids []stackitem.Item + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 9 { + oppID := binary.BigEndian.Uint64(k[1:9]) + ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) + } + return true + }) + + return stackitem.NewArray(ids) +} + +func (c *Collocatio) getOpportunitiesByStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { + status := state.OpportunityStatus(toUint64(args[0])) + + prefix := []byte{collocatioPrefixOpportunityByStatus, byte(status)} + + var ids []stackitem.Item + ic.DAO.Seek(c.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 9 { + oppID := binary.BigEndian.Uint64(k[1:9]) + ids = append(ids, stackitem.NewBigInteger(new(big.Int).SetUint64(oppID))) + } + return true + }) + + return stackitem.NewArray(ids) +} + +// ============================================================================ +// Stack Item Converters +// ============================================================================ + +func opportunityToStackItem(opp *state.InvestmentOpportunity) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Type))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.Status))), + stackitem.NewByteArray(opp.Creator.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.SponsorVitaID)), + stackitem.NewByteArray([]byte(opp.Name)), + stackitem.NewByteArray([]byte(opp.Description)), + stackitem.NewByteArray(opp.TermsHash.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.CurrentParticipants)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MinInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.MaxInvestment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TotalPool)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.TargetPool)), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ExpectedReturns)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.RiskLevel))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.VotingDeadline))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.InvestmentDeadline))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.MaturityDate))), + stackitem.NewBigInteger(new(big.Int).SetUint64(opp.ProposalID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(opp.UpdatedAt))), + }) +} + +func investmentToStackItem(inv *state.Investment) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.OpportunityID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.VitaID)), + stackitem.NewByteArray(inv.Investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.Amount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.Status))), + stackitem.NewBigInteger(new(big.Int).SetUint64(inv.ReturnAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(inv.UpdatedAt))), + }) +} + +func eligibilityToStackItem(elig *state.InvestorEligibility) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.VitaID)), + stackitem.NewByteArray(elig.Investor.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.Eligibility))), + stackitem.NewBool(elig.ScireCompleted), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.RiskScore))), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalInvested)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.TotalReturns)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.ActiveInvestments)), + stackitem.NewBigInteger(new(big.Int).SetUint64(elig.CompletedInvestments)), + stackitem.NewBool(elig.HasViolations), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.ViolationCount))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.LastActivity))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(elig.UpdatedAt))), + }) +} + +func violationToStackItem(v *state.InvestmentViolation) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(v.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(v.VitaID)), + stackitem.NewByteArray(v.Violator.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(v.OpportunityID)), + stackitem.NewByteArray([]byte(v.ViolationType)), + stackitem.NewByteArray([]byte(v.Description)), + stackitem.NewByteArray(v.EvidenceHash.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(v.Penalty)), + stackitem.NewByteArray(v.ReportedBy.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ReportedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(v.ResolvedAt))), + stackitem.NewByteArray([]byte(v.Resolution)), + }) +} + +func employmentToStackItem(ev *state.EmploymentVerification) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(ev.VitaID)), + stackitem.NewByteArray(ev.Employee.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(ev.EmployerVitaID)), + stackitem.NewByteArray(ev.Employer.BytesBE()), + stackitem.NewByteArray([]byte(ev.Position)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.StartDate))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.EndDate))), + stackitem.NewBool(ev.IsActive), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(ev.VerifiedAt))), + stackitem.NewByteArray(ev.VerifiedBy.BytesBE()), + }) +} + +func contractorToStackItem(cv *state.ContractorVerification) stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(cv.VitaID)), + stackitem.NewByteArray(cv.Contractor.BytesBE()), + stackitem.NewByteArray([]byte(cv.PlatformID)), + stackitem.NewByteArray(cv.Platform.BytesBE()), + stackitem.NewByteArray([]byte(cv.ContractorID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.StartDate))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.EndDate))), + stackitem.NewBool(cv.IsActive), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(cv.VerifiedAt))), + stackitem.NewByteArray(cv.VerifiedBy.BytesBE()), + }) +} diff --git a/pkg/core/native/config_registry.go b/pkg/core/native/config_registry.go new file mode 100644 index 0000000..39cdedc --- /dev/null +++ b/pkg/core/native/config_registry.go @@ -0,0 +1,504 @@ +package native + +import ( + "encoding/binary" + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/core/storage" + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// LOW-003: Configuration Governance +// Centralized configuration registry for managing system-wide parameters. +// All hardcoded values should be moved here for transparent governance. + +// ConfigCategory represents a category of configuration values. +type ConfigCategory uint8 + +const ( + // ConfigCategorySystem covers core system parameters + ConfigCategorySystem ConfigCategory = iota + // ConfigCategoryIdentity covers Vita/identity parameters + ConfigCategoryIdentity + // ConfigCategoryEconomic covers VTS/Tribute/economic parameters + ConfigCategoryEconomic + // ConfigCategoryGovernance covers voting/committee parameters + ConfigCategoryGovernance + // ConfigCategoryHealth covers Salus/healthcare parameters + ConfigCategoryHealth + // ConfigCategoryEducation covers Scire/education parameters + ConfigCategoryEducation + // ConfigCategoryLegal covers Lex/legal parameters + ConfigCategoryLegal + // ConfigCategorySecurity covers security-related parameters + ConfigCategorySecurity +) + +// ConfigEntry represents a single configuration value. +type ConfigEntry struct { + Key string // Unique key (category:name format) + Category ConfigCategory // Category for grouping + ValueType ConfigValueType + CurrentValue []byte // Current value (serialized) + DefaultValue []byte // Default value (for reference) + MinValue []byte // Minimum allowed value (if applicable) + MaxValue []byte // Maximum allowed value (if applicable) + Description string // Human-readable description + RequiresVote bool // Whether changes require governance vote + LastModified uint32 // Block height of last modification + ModifiedBy util.Uint160 // Who made the last modification +} + +// ConfigValueType represents the type of a configuration value. +type ConfigValueType uint8 + +const ( + ConfigTypeUint64 ConfigValueType = iota + ConfigTypeInt64 + ConfigTypeBool + ConfigTypeString + ConfigTypeHash160 + ConfigTypeHash256 + ConfigTypeBytes +) + +// ConfigChangeProposal represents a pending configuration change. +type ConfigChangeProposal struct { + ProposalID uint64 + Key string + NewValue []byte + Proposer util.Uint160 + ProposedAt uint32 + ExpiresAt uint32 + Approvals []util.Uint160 + RequiredVotes uint32 + Status ConfigProposalStatus +} + +// ConfigProposalStatus represents the status of a config change proposal. +type ConfigProposalStatus uint8 + +const ( + ConfigProposalPending ConfigProposalStatus = iota + ConfigProposalApproved + ConfigProposalRejected + ConfigProposalExpired + ConfigProposalExecuted +) + +// Storage prefixes for configuration registry. +const ( + configRegPrefixEntry byte = 0xC0 // key -> ConfigEntry + configRegPrefixByCategory byte = 0xC1 // category + key -> exists + configRegPrefixProposal byte = 0xC2 // proposalID -> ConfigChangeProposal + configRegPrefixProposalCtr byte = 0xCF // -> next proposalID +) + +// Configuration errors. +var ( + ErrConfigNotFound = errors.New("configuration key not found") + ErrConfigInvalidValue = errors.New("invalid configuration value") + ErrConfigOutOfRange = errors.New("configuration value out of allowed range") + ErrConfigRequiresVote = errors.New("configuration change requires governance vote") + ErrConfigProposalExists = errors.New("pending proposal already exists for this key") +) + +// ConfigRegistry manages system-wide configuration. +type ConfigRegistry struct { + contractID int32 +} + +// NewConfigRegistry creates a new configuration registry. +func NewConfigRegistry(contractID int32) *ConfigRegistry { + return &ConfigRegistry{contractID: contractID} +} + +// GetEntry retrieves a configuration entry by key. +func (cr *ConfigRegistry) GetEntry(d *dao.Simple, key string) *ConfigEntry { + storageKey := cr.makeEntryKey(key) + si := d.GetStorageItem(cr.contractID, storageKey) + if si == nil { + return nil + } + return cr.deserializeEntry(si) +} + +// SetEntry stores a configuration entry. +func (cr *ConfigRegistry) SetEntry(d *dao.Simple, entry *ConfigEntry) { + storageKey := cr.makeEntryKey(entry.Key) + data := cr.serializeEntry(entry) + d.PutStorageItem(cr.contractID, storageKey, data) + + // Also index by category + catKey := cr.makeCategoryKey(entry.Category, entry.Key) + d.PutStorageItem(cr.contractID, catKey, []byte{1}) +} + +// GetUint64 retrieves a uint64 configuration value. +func (cr *ConfigRegistry) GetUint64(d *dao.Simple, key string) (uint64, error) { + entry := cr.GetEntry(d, key) + if entry == nil { + return 0, ErrConfigNotFound + } + if entry.ValueType != ConfigTypeUint64 || len(entry.CurrentValue) < 8 { + return 0, ErrConfigInvalidValue + } + return binary.BigEndian.Uint64(entry.CurrentValue), nil +} + +// GetUint64OrDefault retrieves a uint64 value or returns default if not found. +func (cr *ConfigRegistry) GetUint64OrDefault(d *dao.Simple, key string, defaultVal uint64) uint64 { + val, err := cr.GetUint64(d, key) + if err != nil { + return defaultVal + } + return val +} + +// SetUint64 sets a uint64 configuration value. +func (cr *ConfigRegistry) SetUint64(d *dao.Simple, key string, value uint64, modifier util.Uint160, blockHeight uint32) error { + entry := cr.GetEntry(d, key) + if entry == nil { + return ErrConfigNotFound + } + if entry.ValueType != ConfigTypeUint64 { + return ErrConfigInvalidValue + } + + // Validate range + if len(entry.MinValue) >= 8 && len(entry.MaxValue) >= 8 { + minVal := binary.BigEndian.Uint64(entry.MinValue) + maxVal := binary.BigEndian.Uint64(entry.MaxValue) + if value < minVal || value > maxVal { + return ErrConfigOutOfRange + } + } + + entry.CurrentValue = make([]byte, 8) + binary.BigEndian.PutUint64(entry.CurrentValue, value) + entry.LastModified = blockHeight + entry.ModifiedBy = modifier + cr.SetEntry(d, entry) + return nil +} + +// GetBool retrieves a boolean configuration value. +func (cr *ConfigRegistry) GetBool(d *dao.Simple, key string) (bool, error) { + entry := cr.GetEntry(d, key) + if entry == nil { + return false, ErrConfigNotFound + } + if entry.ValueType != ConfigTypeBool || len(entry.CurrentValue) < 1 { + return false, ErrConfigInvalidValue + } + return entry.CurrentValue[0] == 1, nil +} + +// GetBoolOrDefault retrieves a bool value or returns default if not found. +func (cr *ConfigRegistry) GetBoolOrDefault(d *dao.Simple, key string, defaultVal bool) bool { + val, err := cr.GetBool(d, key) + if err != nil { + return defaultVal + } + return val +} + +// GetEntriesByCategory retrieves all entries in a category. +func (cr *ConfigRegistry) GetEntriesByCategory(d *dao.Simple, category ConfigCategory) []*ConfigEntry { + var entries []*ConfigEntry + prefix := []byte{configRegPrefixByCategory, byte(category)} + + d.Seek(cr.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) > 1 { + key := string(k[1:]) // Skip category byte + entry := cr.GetEntry(d, key) + if entry != nil { + entries = append(entries, entry) + } + } + return true + }) + + return entries +} + +// RegisterConfig registers a new configuration entry with defaults. +func (cr *ConfigRegistry) RegisterConfig(d *dao.Simple, key string, category ConfigCategory, valueType ConfigValueType, + defaultValue []byte, minValue, maxValue []byte, description string, requiresVote bool) { + + entry := &ConfigEntry{ + Key: key, + Category: category, + ValueType: valueType, + CurrentValue: defaultValue, + DefaultValue: defaultValue, + MinValue: minValue, + MaxValue: maxValue, + Description: description, + RequiresVote: requiresVote, + } + cr.SetEntry(d, entry) +} + +// ToStackItem converts ConfigEntry to stack item for RPC. +func (e *ConfigEntry) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray([]byte(e.Key)), + stackitem.NewBigInteger(big.NewInt(int64(e.Category))), + stackitem.NewBigInteger(big.NewInt(int64(e.ValueType))), + stackitem.NewByteArray(e.CurrentValue), + stackitem.NewByteArray(e.DefaultValue), + stackitem.NewByteArray([]byte(e.Description)), + stackitem.NewBool(e.RequiresVote), + stackitem.NewBigInteger(big.NewInt(int64(e.LastModified))), + }) +} + +// Helper methods for storage keys. +func (cr *ConfigRegistry) makeEntryKey(key string) []byte { + result := make([]byte, 1+len(key)) + result[0] = configRegPrefixEntry + copy(result[1:], key) + return result +} + +func (cr *ConfigRegistry) makeCategoryKey(category ConfigCategory, key string) []byte { + result := make([]byte, 2+len(key)) + result[0] = configRegPrefixByCategory + result[1] = byte(category) + copy(result[2:], key) + return result +} + +// Serialization helpers. +func (cr *ConfigRegistry) serializeEntry(e *ConfigEntry) []byte { + keyBytes := []byte(e.Key) + descBytes := []byte(e.Description) + + size := 4 + len(keyBytes) + // key length + key + 1 + // category + 1 + // value type + 4 + len(e.CurrentValue) + // current value length + value + 4 + len(e.DefaultValue) + // default value length + value + 4 + len(e.MinValue) + // min value length + value + 4 + len(e.MaxValue) + // max value length + value + 4 + len(descBytes) + // description length + desc + 1 + // requires vote + 4 + // last modified + 20 // modified by + + data := make([]byte, size) + offset := 0 + + // Key + binary.BigEndian.PutUint32(data[offset:], uint32(len(keyBytes))) + offset += 4 + copy(data[offset:], keyBytes) + offset += len(keyBytes) + + // Category + data[offset] = byte(e.Category) + offset++ + + // Value type + data[offset] = byte(e.ValueType) + offset++ + + // Current value + binary.BigEndian.PutUint32(data[offset:], uint32(len(e.CurrentValue))) + offset += 4 + copy(data[offset:], e.CurrentValue) + offset += len(e.CurrentValue) + + // Default value + binary.BigEndian.PutUint32(data[offset:], uint32(len(e.DefaultValue))) + offset += 4 + copy(data[offset:], e.DefaultValue) + offset += len(e.DefaultValue) + + // Min value + binary.BigEndian.PutUint32(data[offset:], uint32(len(e.MinValue))) + offset += 4 + copy(data[offset:], e.MinValue) + offset += len(e.MinValue) + + // Max value + binary.BigEndian.PutUint32(data[offset:], uint32(len(e.MaxValue))) + offset += 4 + copy(data[offset:], e.MaxValue) + offset += len(e.MaxValue) + + // Description + binary.BigEndian.PutUint32(data[offset:], uint32(len(descBytes))) + offset += 4 + copy(data[offset:], descBytes) + offset += len(descBytes) + + // Requires vote + if e.RequiresVote { + data[offset] = 1 + } + offset++ + + // Last modified + binary.BigEndian.PutUint32(data[offset:], e.LastModified) + offset += 4 + + // Modified by + copy(data[offset:], e.ModifiedBy.BytesBE()) + + return data +} + +func (cr *ConfigRegistry) deserializeEntry(data []byte) *ConfigEntry { + if len(data) < 10 { + return nil + } + + e := &ConfigEntry{} + offset := 0 + + // Key + keyLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(keyLen) > len(data) { + return nil + } + e.Key = string(data[offset : offset+int(keyLen)]) + offset += int(keyLen) + + // Category + e.Category = ConfigCategory(data[offset]) + offset++ + + // Value type + e.ValueType = ConfigValueType(data[offset]) + offset++ + + // Current value + if offset+4 > len(data) { + return nil + } + valLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(valLen) > len(data) { + return nil + } + e.CurrentValue = make([]byte, valLen) + copy(e.CurrentValue, data[offset:offset+int(valLen)]) + offset += int(valLen) + + // Default value + if offset+4 > len(data) { + return nil + } + defLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(defLen) > len(data) { + return nil + } + e.DefaultValue = make([]byte, defLen) + copy(e.DefaultValue, data[offset:offset+int(defLen)]) + offset += int(defLen) + + // Min value + if offset+4 > len(data) { + return nil + } + minLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(minLen) > len(data) { + return nil + } + e.MinValue = make([]byte, minLen) + copy(e.MinValue, data[offset:offset+int(minLen)]) + offset += int(minLen) + + // Max value + if offset+4 > len(data) { + return nil + } + maxLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(maxLen) > len(data) { + return nil + } + e.MaxValue = make([]byte, maxLen) + copy(e.MaxValue, data[offset:offset+int(maxLen)]) + offset += int(maxLen) + + // Description + if offset+4 > len(data) { + return nil + } + descLen := binary.BigEndian.Uint32(data[offset:]) + offset += 4 + if offset+int(descLen) > len(data) { + return nil + } + e.Description = string(data[offset : offset+int(descLen)]) + offset += int(descLen) + + // Requires vote + if offset >= len(data) { + return nil + } + e.RequiresVote = data[offset] == 1 + offset++ + + // Last modified + if offset+4 > len(data) { + return nil + } + e.LastModified = binary.BigEndian.Uint32(data[offset:]) + offset += 4 + + // Modified by + if offset+20 > len(data) { + return nil + } + e.ModifiedBy, _ = util.Uint160DecodeBytesBE(data[offset : offset+20]) + + return e +} + +// StandardConfigKeys defines standard configuration keys used across contracts. +var StandardConfigKeys = struct { + // System + MaxQueryLimit string + DefaultPageSize string + BlockInterval string + + // Security + RecoveryDelay string + RequiredApprovals string + RateLimitBlocks string + ProofExpiryBlocks string + + // Economic + TributeRateMild string + TributeRateSevere string + AsylumQuotaPerYear string + + // Governance + VotingAge string + ProposalQuorum string + ProposalThreshold string +}{ + MaxQueryLimit: "system:max_query_limit", + DefaultPageSize: "system:default_page_size", + BlockInterval: "system:block_interval", + RecoveryDelay: "security:recovery_delay", + RequiredApprovals: "security:required_approvals", + RateLimitBlocks: "security:rate_limit_blocks", + ProofExpiryBlocks: "security:proof_expiry_blocks", + TributeRateMild: "economic:tribute_rate_mild", + TributeRateSevere: "economic:tribute_rate_severe", + AsylumQuotaPerYear: "economic:asylum_quota_per_year", + VotingAge: "governance:voting_age", + ProposalQuorum: "governance:proposal_quorum", + ProposalThreshold: "governance:proposal_threshold", +} diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go old mode 100644 new mode 100755 index af9a0ba..f7cd815 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -138,6 +138,9 @@ type ( HasRoleInternal(d *dao.Simple, address util.Uint160, roleID uint64, blockHeight uint32) bool // HasPermissionInternal checks if address has permission via roles. HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool + // HasDomainCommitteeAuthority checks if address has committee authority for a specific domain. + // CRIT-002: Domain-specific committee for reduced single point of failure. + HasDomainCommitteeAuthority(d *dao.Simple, address util.Uint160, domain CommitteeDomain, blockHeight uint32) bool } // IVTS is an interface required from native VTS contract for diff --git a/pkg/core/native/eligere.go b/pkg/core/native/eligere.go old mode 100644 new mode 100755 index 62dfdf7..42d2e15 --- a/pkg/core/native/eligere.go +++ b/pkg/core/native/eligere.go @@ -1,950 +1,950 @@ -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/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" -) - -// Storage key prefixes for Eligere. -const ( - eligerePrefixProposal byte = 0x01 // proposalID -> Proposal - eligerePrefixProposalTitle byte = 0x02 // titleHash -> proposalID - eligerePrefixVote byte = 0x03 // proposalID + vitaID -> Vote - eligerePrefixVoterHistory byte = 0x04 // vitaID + proposalID -> exists - eligerePrefixProposalStatus byte = 0x05 // status + proposalID -> exists - eligerePrefixCategory byte = 0x06 // category + proposalID -> exists - eligerePrefixActiveProposals byte = 0x07 // -> serialized []proposalID - eligerePrefixProposalCounter byte = 0xF0 // -> next proposalID - eligerePrefixConfig byte = 0xF1 // -> EligereConfig -) - -// Event names for Eligere. -const ( - ProposalCreatedEvent = "ProposalCreated" - ProposalActivatedEvent = "ProposalActivated" - VoteCastEvent = "VoteCast" - ProposalPassedEvent = "ProposalPassed" - ProposalRejectedEvent = "ProposalRejected" - ProposalExecutedEvent = "ProposalExecuted" - ProposalCancelledEvent = "ProposalCancelled" - ProposalExpiredEvent = "ProposalExpired" -) - -// Errors for Eligere. -var ( - ErrProposalNotFound = errors.New("proposal not found") - ErrProposalNotActive = errors.New("proposal is not active for voting") - ErrAlreadyVoted = errors.New("already voted on this proposal") - ErrVotingNotStarted = errors.New("voting period has not started") - ErrVotingEnded = errors.New("voting period has ended") - ErrNotProposer = errors.New("caller is not the proposer") - ErrProposalTitleExists = errors.New("proposal title already exists") - ErrInvalidCategory = errors.New("invalid proposal category") - ErrQuorumNotMet = errors.New("quorum not met") - ErrThresholdNotMet = errors.New("threshold not met") - ErrProposalNotPassed = errors.New("proposal has not passed") - ErrExecutionDelayNotPassed = errors.New("execution delay has not passed") - ErrProposalAlreadyExecuted = errors.New("proposal already executed") - ErrNoVitaToken = errors.New("caller must have active Vita") - ErrVotingRightRestricted = errors.New("voting right is restricted") - ErrUnderVotingAge = errors.New("voter is under voting age") - ErrInvalidVotingPeriod = errors.New("invalid voting period") - ErrTitleTooLong = errors.New("title too long (max 128 chars)") - ErrInvalidVoteChoice = errors.New("invalid vote choice") - ErrVotingPeriodNotEnded = errors.New("voting period has not ended") - ErrProposalAlreadyFinalized = errors.New("proposal already finalized") - ErrNotCommitteeOrProposer = errors.New("only proposer or committee can cancel") - ErrCannotCancelFinalized = errors.New("cannot cancel finalized proposal") -) - -// Eligere represents the democratic voting native contract. -type Eligere struct { - interop.ContractMD - - Tutus ITutus - Vita IVita - RoleRegistry IRoleRegistry - Lex ILex - Annos IAnnos -} - -// EligereCache contains cached data for performance. -type EligereCache struct { - proposalCount uint64 -} - -// Copy implements dao.NativeContractCache. -func (c *EligereCache) Copy() dao.NativeContractCache { - return &EligereCache{ - proposalCount: c.proposalCount, - } -} - -// newEligere creates a new Eligere native contract. -func newEligere() *Eligere { - e := &Eligere{} - - e.ContractMD = *interop.NewContractMD(nativenames.Eligere, nativeids.Eligere) - defer e.BuildHFSpecificMD(e.ActiveIn()) - - // ============ Proposal Management Methods ============ - - desc := NewDescriptor("createProposal", smartcontract.IntegerType, - manifest.NewParameter("title", smartcontract.StringType), - manifest.NewParameter("contentHash", smartcontract.ByteArrayType), - manifest.NewParameter("category", smartcontract.IntegerType), - manifest.NewParameter("votingStartsAt", smartcontract.IntegerType), - manifest.NewParameter("votingEndsAt", smartcontract.IntegerType), - manifest.NewParameter("targetContract", smartcontract.Hash160Type), - manifest.NewParameter("targetMethod", smartcontract.StringType), - manifest.NewParameter("targetParams", smartcontract.ByteArrayType)) - md := NewMethodAndPrice(e.createProposal, 1<<15, callflag.States|callflag.AllowNotify) - e.AddMethod(md, desc) - - desc = NewDescriptor("cancelProposal", smartcontract.BoolType, - manifest.NewParameter("proposalID", smartcontract.IntegerType)) - md = NewMethodAndPrice(e.cancelProposal, 1<<15, callflag.States|callflag.AllowNotify) - e.AddMethod(md, desc) - - // ============ Voting Methods ============ - - desc = NewDescriptor("vote", smartcontract.BoolType, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("choice", smartcontract.IntegerType)) - md = NewMethodAndPrice(e.vote, 1<<15, callflag.States|callflag.AllowNotify) - e.AddMethod(md, desc) - - // ============ Tallying & Execution Methods ============ - - desc = NewDescriptor("tallyVotes", smartcontract.BoolType, - manifest.NewParameter("proposalID", smartcontract.IntegerType)) - md = NewMethodAndPrice(e.tallyVotes, 1<<15, callflag.States|callflag.AllowNotify) - e.AddMethod(md, desc) - - desc = NewDescriptor("executeProposal", smartcontract.BoolType, - manifest.NewParameter("proposalID", smartcontract.IntegerType)) - md = NewMethodAndPrice(e.executeProposal, 1<<17, callflag.All) - e.AddMethod(md, desc) - - // ============ Query Methods ============ - - desc = NewDescriptor("getProposal", smartcontract.ArrayType, - manifest.NewParameter("proposalID", smartcontract.IntegerType)) - md = NewMethodAndPrice(e.getProposal, 1<<15, callflag.ReadStates) - e.AddMethod(md, desc) - - desc = NewDescriptor("hasVoted", smartcontract.BoolType, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("voter", smartcontract.Hash160Type)) - md = NewMethodAndPrice(e.hasVoted, 1<<15, callflag.ReadStates) - e.AddMethod(md, desc) - - desc = NewDescriptor("getVote", smartcontract.ArrayType, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("voter", smartcontract.Hash160Type)) - md = NewMethodAndPrice(e.getVote, 1<<15, callflag.ReadStates) - e.AddMethod(md, desc) - - desc = NewDescriptor("getProposalCount", smartcontract.IntegerType) - md = NewMethodAndPrice(e.getProposalCount, 1<<15, callflag.ReadStates) - e.AddMethod(md, desc) - - desc = NewDescriptor("getConfig", smartcontract.ArrayType) - md = NewMethodAndPrice(e.getConfig, 1<<15, callflag.ReadStates) - e.AddMethod(md, desc) - - // ============ Events ============ - - eDesc := NewEventDescriptor(ProposalCreatedEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("title", smartcontract.StringType), - manifest.NewParameter("category", smartcontract.IntegerType), - manifest.NewParameter("proposer", smartcontract.Hash160Type), - manifest.NewParameter("votingEndsAt", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalActivatedEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(VoteCastEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("voterVitaID", smartcontract.IntegerType), - manifest.NewParameter("choice", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalPassedEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("supportPercent", smartcontract.IntegerType), - manifest.NewParameter("totalVotes", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalRejectedEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("supportPercent", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalExecutedEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("executor", smartcontract.Hash160Type), - manifest.NewParameter("executedAt", smartcontract.IntegerType)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalCancelledEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("cancelledBy", smartcontract.Hash160Type)) - e.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(ProposalExpiredEvent, - manifest.NewParameter("proposalID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - e.AddEvent(NewEvent(eDesc)) - - return e -} - -// Initialize implements the Contract interface. -func (e *Eligere) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != e.ActiveIn() { - return nil - } - - // Initialize cache - cache := &EligereCache{proposalCount: 0} - ic.DAO.SetCache(e.ID, cache) - - // Initialize config with defaults - cfg := state.DefaultEligereConfig() - if err := e.putConfig(ic.DAO, cfg); err != nil { - return err - } - - // Initialize proposal counter - setIntWithKey(e.ID, ic.DAO, []byte{eligerePrefixProposalCounter}, 0) - - return nil -} - -// InitializeCache implements the Contract interface. -func (e *Eligere) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - cache := &EligereCache{ - proposalCount: e.getProposalCounter(d), - } - d.SetCache(e.ID, cache) - return nil -} - -// OnPersist implements the Contract interface. -func (e *Eligere) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist implements the Contract interface. -func (e *Eligere) PostPersist(ic *interop.Context) error { - return nil -} - -// Metadata returns contract metadata. -func (e *Eligere) Metadata() *interop.ContractMD { - return &e.ContractMD -} - -// Address returns the contract's script hash. -func (e *Eligere) Address() util.Uint160 { - return e.Hash -} - -// GetProposalInternal returns a proposal by ID (for cross-native access). -func (e *Eligere) GetProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { - return e.getProposalInternal(d, proposalID) -} - -// ActiveIn implements the Contract interface. -func (e *Eligere) ActiveIn() *config.Hardfork { - return nil // Active from genesis -} - -// ============ Storage Helpers ============ - -func (e *Eligere) makeProposalKey(proposalID uint64) []byte { - key := make([]byte, 1+8) - key[0] = eligerePrefixProposal - binary.BigEndian.PutUint64(key[1:], proposalID) - return key -} - -func (e *Eligere) makeTitleKey(title string) []byte { - h := hash.Hash160([]byte(title)) - key := make([]byte, 1+20) - key[0] = eligerePrefixProposalTitle - copy(key[1:], h.BytesBE()) - return key -} - -func (e *Eligere) makeVoteKey(proposalID, vitaID uint64) []byte { - key := make([]byte, 1+8+8) - key[0] = eligerePrefixVote - binary.BigEndian.PutUint64(key[1:9], proposalID) - binary.BigEndian.PutUint64(key[9:], vitaID) - return key -} - -func (e *Eligere) makeVoterHistoryKey(vitaID, proposalID uint64) []byte { - key := make([]byte, 1+8+8) - key[0] = eligerePrefixVoterHistory - binary.BigEndian.PutUint64(key[1:9], vitaID) - binary.BigEndian.PutUint64(key[9:], proposalID) - return key -} - -func (e *Eligere) makeStatusIndexKey(status state.ProposalStatus, proposalID uint64) []byte { - key := make([]byte, 1+1+8) - key[0] = eligerePrefixProposalStatus - key[1] = byte(status) - binary.BigEndian.PutUint64(key[2:], proposalID) - return key -} - -func (e *Eligere) makeCategoryIndexKey(category state.ProposalCategory, proposalID uint64) []byte { - key := make([]byte, 1+1+8) - key[0] = eligerePrefixCategory - key[1] = byte(category) - binary.BigEndian.PutUint64(key[2:], proposalID) - return key -} - -// ============ Proposal Storage ============ - -func (e *Eligere) getProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { - key := e.makeProposalKey(proposalID) - proposal := &state.Proposal{} - err := getConvertibleFromDAO(e.ID, d, key, proposal) - if err != nil { - return nil - } - return proposal -} - -func (e *Eligere) putProposal(d *dao.Simple, proposal *state.Proposal) error { - key := e.makeProposalKey(proposal.ID) - return putConvertibleToDAO(e.ID, d, key, proposal) -} - -func (e *Eligere) titleExists(d *dao.Simple, title string) bool { - key := e.makeTitleKey(title) - si := d.GetStorageItem(e.ID, key) - return si != nil -} - -func (e *Eligere) putTitleIndex(d *dao.Simple, title string, proposalID uint64) { - key := e.makeTitleKey(title) - val := make([]byte, 8) - binary.BigEndian.PutUint64(val, proposalID) - d.PutStorageItem(e.ID, key, val) -} - -// ============ Vote Storage ============ - -func (e *Eligere) getVoteInternal(d *dao.Simple, proposalID, vitaID uint64) *state.Vote { - key := e.makeVoteKey(proposalID, vitaID) - vote := &state.Vote{} - err := getConvertibleFromDAO(e.ID, d, key, vote) - if err != nil { - return nil - } - return vote -} - -func (e *Eligere) putVote(d *dao.Simple, vote *state.Vote) error { - key := e.makeVoteKey(vote.ProposalID, vote.VoterVitaID) - return putConvertibleToDAO(e.ID, d, key, vote) -} - -func (e *Eligere) hasVotedInternal(d *dao.Simple, proposalID, vitaID uint64) bool { - key := e.makeVoteKey(proposalID, vitaID) - si := d.GetStorageItem(e.ID, key) - return si != nil -} - -func (e *Eligere) putVoterHistory(d *dao.Simple, vitaID, proposalID uint64) { - key := e.makeVoterHistoryKey(vitaID, proposalID) - d.PutStorageItem(e.ID, key, []byte{1}) -} - -// ============ Config Storage ============ - -func (e *Eligere) getConfigInternal(d *dao.Simple) *state.EligereConfig { - key := []byte{eligerePrefixConfig} - cfg := &state.EligereConfig{} - err := getConvertibleFromDAO(e.ID, d, key, cfg) - if err != nil { - return state.DefaultEligereConfig() - } - return cfg -} - -func (e *Eligere) putConfig(d *dao.Simple, cfg *state.EligereConfig) error { - key := []byte{eligerePrefixConfig} - return putConvertibleToDAO(e.ID, d, key, cfg) -} - -// ============ Counter ============ - -func (e *Eligere) getProposalCounter(d *dao.Simple) uint64 { - key := []byte{eligerePrefixProposalCounter} - return uint64(getIntWithKey(e.ID, d, key)) -} - -func (e *Eligere) getAndIncrementProposalCounter(d *dao.Simple) uint64 { - key := []byte{eligerePrefixProposalCounter} - current := getIntWithKey(e.ID, d, key) - setIntWithKey(e.ID, d, key, current+1) - return uint64(current + 1) -} - -// ============ Status Index ============ - -func (e *Eligere) updateStatusIndex(d *dao.Simple, proposalID uint64, oldStatus, newStatus state.ProposalStatus) { - // Remove from old status index - if oldStatus != newStatus { - oldKey := e.makeStatusIndexKey(oldStatus, proposalID) - d.DeleteStorageItem(e.ID, oldKey) - } - // Add to new status index - newKey := e.makeStatusIndexKey(newStatus, proposalID) - d.PutStorageItem(e.ID, newKey, []byte{1}) -} - -// ============ Authorization Helpers ============ - -func (e *Eligere) checkCommittee(ic *interop.Context) bool { - if e.Tutus == nil { - return false - } - return e.Tutus.CheckCommittee(ic) -} - -func (e *Eligere) hasLegislatorRole(d *dao.Simple, addr util.Uint160, blockHeight uint32) bool { - if e.RoleRegistry == nil { - return false - } - return e.RoleRegistry.HasRoleInternal(d, addr, RoleLegislator, blockHeight) -} - -// ============ Contract Methods ============ - -// createProposal creates a new democratic proposal. -func (e *Eligere) createProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { - title := toString(args[0]) - contentHashBytes := toBytes(args[1]) - category := state.ProposalCategory(toBigInt(args[2]).Uint64()) - votingStartsAt := uint32(toBigInt(args[3]).Uint64()) - votingEndsAt := uint32(toBigInt(args[4]).Uint64()) - targetContract := toUint160(args[5]) - targetMethod := toString(args[6]) - targetParams := toBytes(args[7]) - - caller := ic.VM.GetCallingScriptHash() - - // Validate caller has active Vita - token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) - if err != nil || token == nil || token.Status != state.TokenStatusActive { - panic(ErrNoVitaToken) - } - - // Check voting right is not restricted (via Lex) - if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { - panic(ErrVotingRightRestricted) - } - - // For law/constitutional amendments, require legislator role or committee - if category == state.ProposalCategoryLawAmendment || category == state.ProposalCategoryConstitutional { - if !e.hasLegislatorRole(ic.DAO, caller, ic.Block.Index) && !e.checkCommittee(ic) { - panic("legislative authority required for law amendments") - } - } - - // Validate inputs - if len(title) > 128 { - panic(ErrTitleTooLong) - } - if len(title) == 0 { - panic("title cannot be empty") - } - if category < state.ProposalCategoryLawAmendment || category > state.ProposalCategoryReferendum { - panic(ErrInvalidCategory) - } - - // Check title uniqueness - if e.titleExists(ic.DAO, title) { - panic(ErrProposalTitleExists) - } - - // Validate voting period - cfg := e.getConfigInternal(ic.DAO) - if votingStartsAt < ic.Block.Index { - votingStartsAt = ic.Block.Index // Can't start in past - } - duration := votingEndsAt - votingStartsAt - if duration < cfg.MinVotingPeriod || duration > cfg.MaxVotingPeriod { - panic(ErrInvalidVotingPeriod) - } - - // Determine threshold based on category - threshold := cfg.StandardThreshold - if category == state.ProposalCategoryConstitutional { - threshold = cfg.ConstitutionalThreshold - } - - // Get next proposal ID - proposalID := e.getAndIncrementProposalCounter(ic.DAO) - - // Create proposal - var contentHash util.Uint256 - if len(contentHashBytes) == 32 { - copy(contentHash[:], contentHashBytes) - } - - proposal := &state.Proposal{ - ID: proposalID, - Title: title, - ContentHash: contentHash, - Category: category, - Proposer: caller, - ProposerVitaID: token.TokenID, - CreatedAt: ic.Block.Index, - VotingStartsAt: votingStartsAt, - VotingEndsAt: votingEndsAt, - ExecutionDelay: cfg.DefaultExecutionDelay, - QuorumPercent: cfg.DefaultQuorum, - ThresholdPercent: threshold, - Status: state.ProposalStatusDraft, - TargetContract: targetContract, - TargetMethod: targetMethod, - TargetParams: targetParams, - } - - // If voting starts now, activate immediately - if votingStartsAt <= ic.Block.Index { - proposal.Status = state.ProposalStatusActive - } - - // Store proposal - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - - // Store indexes - e.putTitleIndex(ic.DAO, title, proposalID) - e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, proposal.Status) - - // Store category index - catKey := e.makeCategoryIndexKey(category, proposalID) - ic.DAO.PutStorageItem(e.ID, catKey, []byte{1}) - - // Emit event - ic.AddNotification(e.Hash, ProposalCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewByteArray([]byte(title)), - stackitem.NewBigInteger(big.NewInt(int64(category))), - stackitem.NewByteArray(caller.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(votingEndsAt))), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(proposalID))) -} - -// cancelProposal cancels a proposal (proposer or committee only). -func (e *Eligere) cancelProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - - caller := ic.VM.GetCallingScriptHash() - - proposal := e.getProposalInternal(ic.DAO, proposalID) - if proposal == nil { - panic(ErrProposalNotFound) - } - - // Check authorization: must be proposer or committee - isProposer := proposal.Proposer.Equals(caller) - isCommittee := e.checkCommittee(ic) - if !isProposer && !isCommittee { - panic(ErrNotCommitteeOrProposer) - } - - // Can only cancel draft or active proposals - if proposal.Status != state.ProposalStatusDraft && proposal.Status != state.ProposalStatusActive { - panic(ErrCannotCancelFinalized) - } - - oldStatus := proposal.Status - proposal.Status = state.ProposalStatusCancelled - - // Update proposal - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - - // Update status index - e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) - - // Emit event - ic.AddNotification(e.Hash, ProposalCancelledEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewByteArray(caller.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -// vote casts a vote on a proposal. -func (e *Eligere) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - choice := state.VoteChoice(toBigInt(args[1]).Uint64()) - - caller := ic.VM.GetCallingScriptHash() - - // Validate vote choice - if choice > state.VoteChoiceNo { - panic(ErrInvalidVoteChoice) - } - - // Validate caller has active Vita - token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) - if err != nil || token == nil || token.Status != state.TokenStatusActive { - panic(ErrNoVitaToken) - } - - // Check voting right is not restricted - if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { - panic(ErrVotingRightRestricted) - } - - // Check voter is of voting age (18+) - if e.Annos != nil && !e.Annos.IsVotingAgeInternal(ic.DAO, caller, ic.Block.Timestamp) { - panic(ErrUnderVotingAge) - } - - // Get proposal - proposal := e.getProposalInternal(ic.DAO, proposalID) - if proposal == nil { - panic(ErrProposalNotFound) - } - - // Check proposal is active - if proposal.Status != state.ProposalStatusActive { - // Auto-activate if draft and voting period has started - if proposal.Status == state.ProposalStatusDraft && ic.Block.Index >= proposal.VotingStartsAt { - proposal.Status = state.ProposalStatusActive - e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, state.ProposalStatusActive) - } else { - panic(ErrProposalNotActive) - } - } - - // Check voting period - if ic.Block.Index < proposal.VotingStartsAt { - panic(ErrVotingNotStarted) - } - if ic.Block.Index > proposal.VotingEndsAt { - panic(ErrVotingEnded) - } - - // Check not already voted (using Vita ID for one-person-one-vote) - if e.hasVotedInternal(ic.DAO, proposalID, token.TokenID) { - panic(ErrAlreadyVoted) - } - - // Create vote record - voteRecord := &state.Vote{ - ProposalID: proposalID, - VoterVitaID: token.TokenID, - Voter: caller, - Choice: choice, - VotedAt: ic.Block.Index, - Weight: 1, // Equal voting weight - } - - // Store vote - if err := e.putVote(ic.DAO, voteRecord); err != nil { - panic(err) - } - - // Store voter history - e.putVoterHistory(ic.DAO, token.TokenID, proposalID) - - // Update vote counts (incremental tallying) - proposal.TotalVotes++ - switch choice { - case state.VoteChoiceYes: - proposal.YesVotes++ - case state.VoteChoiceNo: - proposal.NoVotes++ - case state.VoteChoiceAbstain: - proposal.AbstainVotes++ - } - - // Update proposal - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - - // Emit event - ic.AddNotification(e.Hash, VoteCastEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))), - stackitem.NewBigInteger(big.NewInt(int64(choice))), - })) - - return stackitem.NewBool(true) -} - -// tallyVotes finalizes voting after the deadline. -func (e *Eligere) tallyVotes(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - - proposal := e.getProposalInternal(ic.DAO, proposalID) - if proposal == nil { - panic(ErrProposalNotFound) - } - - // Must be after voting period - if ic.Block.Index <= proposal.VotingEndsAt { - panic(ErrVotingPeriodNotEnded) - } - - // Must still be active - if proposal.Status != state.ProposalStatusActive { - panic(ErrProposalAlreadyFinalized) - } - - // Get total eligible voters for quorum calculation - // Use Vita token count as the voter base - totalVoters := e.Vita.GetTotalTokenCount(ic.DAO) - if totalVoters == 0 { - totalVoters = 1 // Avoid division by zero - } - - oldStatus := proposal.Status - - // Check quorum (total votes including abstentions) - quorumRequired := (totalVoters * uint64(proposal.QuorumPercent)) / 100 - if proposal.TotalVotes < quorumRequired { - proposal.Status = state.ProposalStatusExpired - - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) - - ic.AddNotification(e.Hash, ProposalExpiredEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewByteArray([]byte("quorum not met")), - })) - return stackitem.NewBool(false) - } - - // Check threshold (yes votes vs yes+no, abstentions don't count toward threshold) - totalDecisiveVotes := proposal.YesVotes + proposal.NoVotes - var supportPercent uint64 = 0 - if totalDecisiveVotes > 0 { - supportPercent = (proposal.YesVotes * 100) / totalDecisiveVotes - } - - if totalDecisiveVotes == 0 || supportPercent < uint64(proposal.ThresholdPercent) { - proposal.Status = state.ProposalStatusRejected - - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) - - ic.AddNotification(e.Hash, ProposalRejectedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), - })) - return stackitem.NewBool(false) - } - - // Proposal passed - proposal.Status = state.ProposalStatusPassed - - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) - - ic.AddNotification(e.Hash, ProposalPassedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), - stackitem.NewBigInteger(big.NewInt(int64(proposal.TotalVotes))), - })) - - return stackitem.NewBool(true) -} - -// executeProposal executes a passed proposal after the execution delay. -func (e *Eligere) executeProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - - caller := ic.VM.GetCallingScriptHash() - - proposal := e.getProposalInternal(ic.DAO, proposalID) - if proposal == nil { - panic(ErrProposalNotFound) - } - - // Must have passed - if proposal.Status != state.ProposalStatusPassed { - panic(ErrProposalNotPassed) - } - - // Check execution delay has passed - executionBlock := proposal.VotingEndsAt + proposal.ExecutionDelay - if ic.Block.Index < executionBlock { - panic(ErrExecutionDelayNotPassed) - } - - // Already executed check - if proposal.ExecutedAt > 0 { - panic(ErrProposalAlreadyExecuted) - } - - oldStatus := proposal.Status - - // Execute based on category - switch proposal.Category { - case state.ProposalCategoryLawAmendment: - // Call Lex.ratifyAmendment if Lex is available - if e.Lex != nil { - e.executeLawAmendment(ic, proposal) - } - case state.ProposalCategoryConstitutional: - // Constitutional amendments also go through Lex - if e.Lex != nil { - e.executeLawAmendment(ic, proposal) - } - case state.ProposalCategoryGovernanceAction: - // Governance actions may call target contract - // Implementation depends on specific action - case state.ProposalCategoryInvestment: - // Investment proposals - future integration - case state.ProposalCategoryReferendum: - // Referendums are advisory, no automatic execution - } - - // Update proposal status - proposal.Status = state.ProposalStatusExecuted - proposal.ExecutedAt = ic.Block.Index - proposal.ExecutedBy = caller - - if err := e.putProposal(ic.DAO, proposal); err != nil { - panic(err) - } - e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) - - // Emit event - ic.AddNotification(e.Hash, ProposalExecutedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(proposalID))), - stackitem.NewByteArray(caller.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))), - })) - - return stackitem.NewBool(true) -} - -// executeLawAmendment calls Lex to ratify a law amendment. -func (e *Eligere) executeLawAmendment(ic *interop.Context, proposal *state.Proposal) { - // The Lex contract's ratifyAmendment method will be called - // This creates or updates a law based on the proposal - if e.Lex != nil { - // All amendments create Federal-level laws - // Constitutional rights (RightLife, RightLiberty, etc.) are immutable in code - // Constitutional proposals require 67% supermajority but still create Federal laws - lawCategory := state.LawCategoryFederal - - // Call Lex to ratify the amendment - e.Lex.RatifyAmendmentInternal(ic, proposal.ID, proposal.ContentHash, lawCategory, 0) - } -} - -// ============ Query Methods ============ - -func (e *Eligere) getProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - - proposal := e.getProposalInternal(ic.DAO, proposalID) - if proposal == nil { - return stackitem.Null{} - } - - item, err := proposal.ToStackItem() - if err != nil { - return stackitem.Null{} - } - return item -} - -func (e *Eligere) hasVoted(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - voter := toUint160(args[1]) - - // Get voter's Vita token - token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) - if err != nil || token == nil { - return stackitem.NewBool(false) - } - - return stackitem.NewBool(e.hasVotedInternal(ic.DAO, proposalID, token.TokenID)) -} - -func (e *Eligere) getVote(ic *interop.Context, args []stackitem.Item) stackitem.Item { - proposalID := toBigInt(args[0]).Uint64() - voter := toUint160(args[1]) - - // Get voter's Vita token - token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) - if err != nil || token == nil { - return stackitem.Null{} - } - - vote := e.getVoteInternal(ic.DAO, proposalID, token.TokenID) - if vote == nil { - return stackitem.Null{} - } - - item, err := vote.ToStackItem() - if err != nil { - return stackitem.Null{} - } - return item -} - -func (e *Eligere) getProposalCount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - key := []byte{eligerePrefixProposalCounter} - count := getIntWithKey(e.ID, ic.DAO, key) - return stackitem.NewBigInteger(big.NewInt(count)) -} - -func (e *Eligere) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cfg := e.getConfigInternal(ic.DAO) - item, err := cfg.ToStackItem() - if err != nil { - return stackitem.Null{} - } - return item -} +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/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" +) + +// Storage key prefixes for Eligere. +const ( + eligerePrefixProposal byte = 0x01 // proposalID -> Proposal + eligerePrefixProposalTitle byte = 0x02 // titleHash -> proposalID + eligerePrefixVote byte = 0x03 // proposalID + vitaID -> Vote + eligerePrefixVoterHistory byte = 0x04 // vitaID + proposalID -> exists + eligerePrefixProposalStatus byte = 0x05 // status + proposalID -> exists + eligerePrefixCategory byte = 0x06 // category + proposalID -> exists + eligerePrefixActiveProposals byte = 0x07 // -> serialized []proposalID + eligerePrefixProposalCounter byte = 0xF0 // -> next proposalID + eligerePrefixConfig byte = 0xF1 // -> EligereConfig +) + +// Event names for Eligere. +const ( + ProposalCreatedEvent = "ProposalCreated" + ProposalActivatedEvent = "ProposalActivated" + VoteCastEvent = "VoteCast" + ProposalPassedEvent = "ProposalPassed" + ProposalRejectedEvent = "ProposalRejected" + ProposalExecutedEvent = "ProposalExecuted" + ProposalCancelledEvent = "ProposalCancelled" + ProposalExpiredEvent = "ProposalExpired" +) + +// Errors for Eligere. +var ( + ErrProposalNotFound = errors.New("proposal not found") + ErrProposalNotActive = errors.New("proposal is not active for voting") + ErrAlreadyVoted = errors.New("already voted on this proposal") + ErrVotingNotStarted = errors.New("voting period has not started") + ErrVotingEnded = errors.New("voting period has ended") + ErrNotProposer = errors.New("caller is not the proposer") + ErrProposalTitleExists = errors.New("proposal title already exists") + ErrInvalidCategory = errors.New("invalid proposal category") + ErrQuorumNotMet = errors.New("quorum not met") + ErrThresholdNotMet = errors.New("threshold not met") + ErrProposalNotPassed = errors.New("proposal has not passed") + ErrExecutionDelayNotPassed = errors.New("execution delay has not passed") + ErrProposalAlreadyExecuted = errors.New("proposal already executed") + ErrNoVitaToken = errors.New("caller must have active Vita") + ErrVotingRightRestricted = errors.New("voting right is restricted") + ErrUnderVotingAge = errors.New("voter is under voting age") + ErrInvalidVotingPeriod = errors.New("invalid voting period") + ErrTitleTooLong = errors.New("title too long (max 128 chars)") + ErrInvalidVoteChoice = errors.New("invalid vote choice") + ErrVotingPeriodNotEnded = errors.New("voting period has not ended") + ErrProposalAlreadyFinalized = errors.New("proposal already finalized") + ErrNotCommitteeOrProposer = errors.New("only proposer or committee can cancel") + ErrCannotCancelFinalized = errors.New("cannot cancel finalized proposal") +) + +// Eligere represents the democratic voting native contract. +type Eligere struct { + interop.ContractMD + + Tutus ITutus + Vita IVita + RoleRegistry IRoleRegistry + Lex ILex + Annos IAnnos +} + +// EligereCache contains cached data for performance. +type EligereCache struct { + proposalCount uint64 +} + +// Copy implements dao.NativeContractCache. +func (c *EligereCache) Copy() dao.NativeContractCache { + return &EligereCache{ + proposalCount: c.proposalCount, + } +} + +// newEligere creates a new Eligere native contract. +func newEligere() *Eligere { + e := &Eligere{} + + e.ContractMD = *interop.NewContractMD(nativenames.Eligere, nativeids.Eligere) + defer e.BuildHFSpecificMD(e.ActiveIn()) + + // ============ Proposal Management Methods ============ + + desc := NewDescriptor("createProposal", smartcontract.IntegerType, + manifest.NewParameter("title", smartcontract.StringType), + manifest.NewParameter("contentHash", smartcontract.ByteArrayType), + manifest.NewParameter("category", smartcontract.IntegerType), + manifest.NewParameter("votingStartsAt", smartcontract.IntegerType), + manifest.NewParameter("votingEndsAt", smartcontract.IntegerType), + manifest.NewParameter("targetContract", smartcontract.Hash160Type), + manifest.NewParameter("targetMethod", smartcontract.StringType), + manifest.NewParameter("targetParams", smartcontract.ByteArrayType)) + md := NewMethodAndPrice(e.createProposal, 1<<15, callflag.States|callflag.AllowNotify) + e.AddMethod(md, desc) + + desc = NewDescriptor("cancelProposal", smartcontract.BoolType, + manifest.NewParameter("proposalID", smartcontract.IntegerType)) + md = NewMethodAndPrice(e.cancelProposal, 1<<15, callflag.States|callflag.AllowNotify) + e.AddMethod(md, desc) + + // ============ Voting Methods ============ + + desc = NewDescriptor("vote", smartcontract.BoolType, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("choice", smartcontract.IntegerType)) + md = NewMethodAndPrice(e.vote, 1<<15, callflag.States|callflag.AllowNotify) + e.AddMethod(md, desc) + + // ============ Tallying & Execution Methods ============ + + desc = NewDescriptor("tallyVotes", smartcontract.BoolType, + manifest.NewParameter("proposalID", smartcontract.IntegerType)) + md = NewMethodAndPrice(e.tallyVotes, 1<<15, callflag.States|callflag.AllowNotify) + e.AddMethod(md, desc) + + desc = NewDescriptor("executeProposal", smartcontract.BoolType, + manifest.NewParameter("proposalID", smartcontract.IntegerType)) + md = NewMethodAndPrice(e.executeProposal, 1<<17, callflag.All) + e.AddMethod(md, desc) + + // ============ Query Methods ============ + + desc = NewDescriptor("getProposal", smartcontract.ArrayType, + manifest.NewParameter("proposalID", smartcontract.IntegerType)) + md = NewMethodAndPrice(e.getProposal, 1<<15, callflag.ReadStates) + e.AddMethod(md, desc) + + desc = NewDescriptor("hasVoted", smartcontract.BoolType, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("voter", smartcontract.Hash160Type)) + md = NewMethodAndPrice(e.hasVoted, 1<<15, callflag.ReadStates) + e.AddMethod(md, desc) + + desc = NewDescriptor("getVote", smartcontract.ArrayType, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("voter", smartcontract.Hash160Type)) + md = NewMethodAndPrice(e.getVote, 1<<15, callflag.ReadStates) + e.AddMethod(md, desc) + + desc = NewDescriptor("getProposalCount", smartcontract.IntegerType) + md = NewMethodAndPrice(e.getProposalCount, 1<<15, callflag.ReadStates) + e.AddMethod(md, desc) + + desc = NewDescriptor("getConfig", smartcontract.ArrayType) + md = NewMethodAndPrice(e.getConfig, 1<<15, callflag.ReadStates) + e.AddMethod(md, desc) + + // ============ Events ============ + + eDesc := NewEventDescriptor(ProposalCreatedEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("title", smartcontract.StringType), + manifest.NewParameter("category", smartcontract.IntegerType), + manifest.NewParameter("proposer", smartcontract.Hash160Type), + manifest.NewParameter("votingEndsAt", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalActivatedEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(VoteCastEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("voterVitaID", smartcontract.IntegerType), + manifest.NewParameter("choice", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalPassedEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("supportPercent", smartcontract.IntegerType), + manifest.NewParameter("totalVotes", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalRejectedEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("supportPercent", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalExecutedEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("executor", smartcontract.Hash160Type), + manifest.NewParameter("executedAt", smartcontract.IntegerType)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalCancelledEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("cancelledBy", smartcontract.Hash160Type)) + e.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(ProposalExpiredEvent, + manifest.NewParameter("proposalID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + e.AddEvent(NewEvent(eDesc)) + + return e +} + +// Initialize implements the Contract interface. +func (e *Eligere) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != e.ActiveIn() { + return nil + } + + // Initialize cache + cache := &EligereCache{proposalCount: 0} + ic.DAO.SetCache(e.ID, cache) + + // Initialize config with defaults + cfg := state.DefaultEligereConfig() + if err := e.putConfig(ic.DAO, cfg); err != nil { + return err + } + + // Initialize proposal counter + setIntWithKey(e.ID, ic.DAO, []byte{eligerePrefixProposalCounter}, 0) + + return nil +} + +// InitializeCache implements the Contract interface. +func (e *Eligere) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + cache := &EligereCache{ + proposalCount: e.getProposalCounter(d), + } + d.SetCache(e.ID, cache) + return nil +} + +// OnPersist implements the Contract interface. +func (e *Eligere) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the Contract interface. +func (e *Eligere) PostPersist(ic *interop.Context) error { + return nil +} + +// Metadata returns contract metadata. +func (e *Eligere) Metadata() *interop.ContractMD { + return &e.ContractMD +} + +// Address returns the contract's script hash. +func (e *Eligere) Address() util.Uint160 { + return e.Hash +} + +// GetProposalInternal returns a proposal by ID (for cross-native access). +func (e *Eligere) GetProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { + return e.getProposalInternal(d, proposalID) +} + +// ActiveIn implements the Contract interface. +func (e *Eligere) ActiveIn() *config.Hardfork { + return nil // Active from genesis +} + +// ============ Storage Helpers ============ + +func (e *Eligere) makeProposalKey(proposalID uint64) []byte { + key := make([]byte, 1+8) + key[0] = eligerePrefixProposal + binary.BigEndian.PutUint64(key[1:], proposalID) + return key +} + +func (e *Eligere) makeTitleKey(title string) []byte { + h := hash.Hash160([]byte(title)) + key := make([]byte, 1+20) + key[0] = eligerePrefixProposalTitle + copy(key[1:], h.BytesBE()) + return key +} + +func (e *Eligere) makeVoteKey(proposalID, vitaID uint64) []byte { + key := make([]byte, 1+8+8) + key[0] = eligerePrefixVote + binary.BigEndian.PutUint64(key[1:9], proposalID) + binary.BigEndian.PutUint64(key[9:], vitaID) + return key +} + +func (e *Eligere) makeVoterHistoryKey(vitaID, proposalID uint64) []byte { + key := make([]byte, 1+8+8) + key[0] = eligerePrefixVoterHistory + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], proposalID) + return key +} + +func (e *Eligere) makeStatusIndexKey(status state.ProposalStatus, proposalID uint64) []byte { + key := make([]byte, 1+1+8) + key[0] = eligerePrefixProposalStatus + key[1] = byte(status) + binary.BigEndian.PutUint64(key[2:], proposalID) + return key +} + +func (e *Eligere) makeCategoryIndexKey(category state.ProposalCategory, proposalID uint64) []byte { + key := make([]byte, 1+1+8) + key[0] = eligerePrefixCategory + key[1] = byte(category) + binary.BigEndian.PutUint64(key[2:], proposalID) + return key +} + +// ============ Proposal Storage ============ + +func (e *Eligere) getProposalInternal(d *dao.Simple, proposalID uint64) *state.Proposal { + key := e.makeProposalKey(proposalID) + proposal := &state.Proposal{} + err := getConvertibleFromDAO(e.ID, d, key, proposal) + if err != nil { + return nil + } + return proposal +} + +func (e *Eligere) putProposal(d *dao.Simple, proposal *state.Proposal) error { + key := e.makeProposalKey(proposal.ID) + return putConvertibleToDAO(e.ID, d, key, proposal) +} + +func (e *Eligere) titleExists(d *dao.Simple, title string) bool { + key := e.makeTitleKey(title) + si := d.GetStorageItem(e.ID, key) + return si != nil +} + +func (e *Eligere) putTitleIndex(d *dao.Simple, title string, proposalID uint64) { + key := e.makeTitleKey(title) + val := make([]byte, 8) + binary.BigEndian.PutUint64(val, proposalID) + d.PutStorageItem(e.ID, key, val) +} + +// ============ Vote Storage ============ + +func (e *Eligere) getVoteInternal(d *dao.Simple, proposalID, vitaID uint64) *state.Vote { + key := e.makeVoteKey(proposalID, vitaID) + vote := &state.Vote{} + err := getConvertibleFromDAO(e.ID, d, key, vote) + if err != nil { + return nil + } + return vote +} + +func (e *Eligere) putVote(d *dao.Simple, vote *state.Vote) error { + key := e.makeVoteKey(vote.ProposalID, vote.VoterVitaID) + return putConvertibleToDAO(e.ID, d, key, vote) +} + +func (e *Eligere) hasVotedInternal(d *dao.Simple, proposalID, vitaID uint64) bool { + key := e.makeVoteKey(proposalID, vitaID) + si := d.GetStorageItem(e.ID, key) + return si != nil +} + +func (e *Eligere) putVoterHistory(d *dao.Simple, vitaID, proposalID uint64) { + key := e.makeVoterHistoryKey(vitaID, proposalID) + d.PutStorageItem(e.ID, key, []byte{1}) +} + +// ============ Config Storage ============ + +func (e *Eligere) getConfigInternal(d *dao.Simple) *state.EligereConfig { + key := []byte{eligerePrefixConfig} + cfg := &state.EligereConfig{} + err := getConvertibleFromDAO(e.ID, d, key, cfg) + if err != nil { + return state.DefaultEligereConfig() + } + return cfg +} + +func (e *Eligere) putConfig(d *dao.Simple, cfg *state.EligereConfig) error { + key := []byte{eligerePrefixConfig} + return putConvertibleToDAO(e.ID, d, key, cfg) +} + +// ============ Counter ============ + +func (e *Eligere) getProposalCounter(d *dao.Simple) uint64 { + key := []byte{eligerePrefixProposalCounter} + return uint64(getIntWithKey(e.ID, d, key)) +} + +func (e *Eligere) getAndIncrementProposalCounter(d *dao.Simple) uint64 { + key := []byte{eligerePrefixProposalCounter} + current := getIntWithKey(e.ID, d, key) + setIntWithKey(e.ID, d, key, current+1) + return uint64(current + 1) +} + +// ============ Status Index ============ + +func (e *Eligere) updateStatusIndex(d *dao.Simple, proposalID uint64, oldStatus, newStatus state.ProposalStatus) { + // Remove from old status index + if oldStatus != newStatus { + oldKey := e.makeStatusIndexKey(oldStatus, proposalID) + d.DeleteStorageItem(e.ID, oldKey) + } + // Add to new status index + newKey := e.makeStatusIndexKey(newStatus, proposalID) + d.PutStorageItem(e.ID, newKey, []byte{1}) +} + +// ============ Authorization Helpers ============ + +func (e *Eligere) checkCommittee(ic *interop.Context) bool { + if e.Tutus == nil { + return false + } + return e.Tutus.CheckCommittee(ic) +} + +func (e *Eligere) hasLegislatorRole(d *dao.Simple, addr util.Uint160, blockHeight uint32) bool { + if e.RoleRegistry == nil { + return false + } + return e.RoleRegistry.HasRoleInternal(d, addr, RoleLegislator, blockHeight) +} + +// ============ Contract Methods ============ + +// createProposal creates a new democratic proposal. +func (e *Eligere) createProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { + title := toString(args[0]) + contentHashBytes := toBytes(args[1]) + category := state.ProposalCategory(toBigInt(args[2]).Uint64()) + votingStartsAt := uint32(toBigInt(args[3]).Uint64()) + votingEndsAt := uint32(toBigInt(args[4]).Uint64()) + targetContract := toUint160(args[5]) + targetMethod := toString(args[6]) + targetParams := toBytes(args[7]) + + caller := ic.VM.GetCallingScriptHash() + + // Validate caller has active Vita + token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil || token == nil || token.Status != state.TokenStatusActive { + panic(ErrNoVitaToken) + } + + // Check voting right is not restricted (via Lex) + if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { + panic(ErrVotingRightRestricted) + } + + // For law/constitutional amendments, require legislator role or committee + if category == state.ProposalCategoryLawAmendment || category == state.ProposalCategoryConstitutional { + if !e.hasLegislatorRole(ic.DAO, caller, ic.Block.Index) && !e.checkCommittee(ic) { + panic("legislative authority required for law amendments") + } + } + + // Validate inputs + if len(title) > 128 { + panic(ErrTitleTooLong) + } + if len(title) == 0 { + panic("title cannot be empty") + } + if category < state.ProposalCategoryLawAmendment || category > state.ProposalCategoryReferendum { + panic(ErrInvalidCategory) + } + + // Check title uniqueness + if e.titleExists(ic.DAO, title) { + panic(ErrProposalTitleExists) + } + + // Validate voting period + cfg := e.getConfigInternal(ic.DAO) + if votingStartsAt < ic.Block.Index { + votingStartsAt = ic.Block.Index // Can't start in past + } + duration := votingEndsAt - votingStartsAt + if duration < cfg.MinVotingPeriod || duration > cfg.MaxVotingPeriod { + panic(ErrInvalidVotingPeriod) + } + + // Determine threshold based on category + threshold := cfg.StandardThreshold + if category == state.ProposalCategoryConstitutional { + threshold = cfg.ConstitutionalThreshold + } + + // Get next proposal ID + proposalID := e.getAndIncrementProposalCounter(ic.DAO) + + // Create proposal + var contentHash util.Uint256 + if len(contentHashBytes) == 32 { + copy(contentHash[:], contentHashBytes) + } + + proposal := &state.Proposal{ + ID: proposalID, + Title: title, + ContentHash: contentHash, + Category: category, + Proposer: caller, + ProposerVitaID: token.TokenID, + CreatedAt: ic.Block.Index, + VotingStartsAt: votingStartsAt, + VotingEndsAt: votingEndsAt, + ExecutionDelay: cfg.DefaultExecutionDelay, + QuorumPercent: cfg.DefaultQuorum, + ThresholdPercent: threshold, + Status: state.ProposalStatusDraft, + TargetContract: targetContract, + TargetMethod: targetMethod, + TargetParams: targetParams, + } + + // If voting starts now, activate immediately + if votingStartsAt <= ic.Block.Index { + proposal.Status = state.ProposalStatusActive + } + + // Store proposal + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + + // Store indexes + e.putTitleIndex(ic.DAO, title, proposalID) + e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, proposal.Status) + + // Store category index + catKey := e.makeCategoryIndexKey(category, proposalID) + ic.DAO.PutStorageItem(e.ID, catKey, []byte{1}) + + // Emit event + ic.AddNotification(e.Hash, ProposalCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewByteArray([]byte(title)), + stackitem.NewBigInteger(big.NewInt(int64(category))), + stackitem.NewByteArray(caller.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(votingEndsAt))), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(proposalID))) +} + +// cancelProposal cancels a proposal (proposer or committee only). +func (e *Eligere) cancelProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + + caller := ic.VM.GetCallingScriptHash() + + proposal := e.getProposalInternal(ic.DAO, proposalID) + if proposal == nil { + panic(ErrProposalNotFound) + } + + // Check authorization: must be proposer or committee + isProposer := proposal.Proposer.Equals(caller) + isCommittee := e.checkCommittee(ic) + if !isProposer && !isCommittee { + panic(ErrNotCommitteeOrProposer) + } + + // Can only cancel draft or active proposals + if proposal.Status != state.ProposalStatusDraft && proposal.Status != state.ProposalStatusActive { + panic(ErrCannotCancelFinalized) + } + + oldStatus := proposal.Status + proposal.Status = state.ProposalStatusCancelled + + // Update proposal + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + + // Update status index + e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) + + // Emit event + ic.AddNotification(e.Hash, ProposalCancelledEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewByteArray(caller.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +// vote casts a vote on a proposal. +func (e *Eligere) vote(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + choice := state.VoteChoice(toBigInt(args[1]).Uint64()) + + caller := ic.VM.GetCallingScriptHash() + + // Validate vote choice + if choice > state.VoteChoiceNo { + panic(ErrInvalidVoteChoice) + } + + // Validate caller has active Vita + token, err := e.Vita.GetTokenByOwner(ic.DAO, caller) + if err != nil || token == nil || token.Status != state.TokenStatusActive { + panic(ErrNoVitaToken) + } + + // Check voting right is not restricted + if e.Lex != nil && !e.Lex.HasRightInternal(ic.DAO, caller, state.RightVote, ic.Block.Index) { + panic(ErrVotingRightRestricted) + } + + // Check voter is of voting age (18+) + if e.Annos != nil && !e.Annos.IsVotingAgeInternal(ic.DAO, caller, ic.Block.Timestamp) { + panic(ErrUnderVotingAge) + } + + // Get proposal + proposal := e.getProposalInternal(ic.DAO, proposalID) + if proposal == nil { + panic(ErrProposalNotFound) + } + + // Check proposal is active + if proposal.Status != state.ProposalStatusActive { + // Auto-activate if draft and voting period has started + if proposal.Status == state.ProposalStatusDraft && ic.Block.Index >= proposal.VotingStartsAt { + proposal.Status = state.ProposalStatusActive + e.updateStatusIndex(ic.DAO, proposalID, state.ProposalStatusDraft, state.ProposalStatusActive) + } else { + panic(ErrProposalNotActive) + } + } + + // Check voting period + if ic.Block.Index < proposal.VotingStartsAt { + panic(ErrVotingNotStarted) + } + if ic.Block.Index > proposal.VotingEndsAt { + panic(ErrVotingEnded) + } + + // Check not already voted (using Vita ID for one-person-one-vote) + if e.hasVotedInternal(ic.DAO, proposalID, token.TokenID) { + panic(ErrAlreadyVoted) + } + + // Create vote record + voteRecord := &state.Vote{ + ProposalID: proposalID, + VoterVitaID: token.TokenID, + Voter: caller, + Choice: choice, + VotedAt: ic.Block.Index, + Weight: 1, // Equal voting weight + } + + // Store vote + if err := e.putVote(ic.DAO, voteRecord); err != nil { + panic(err) + } + + // Store voter history + e.putVoterHistory(ic.DAO, token.TokenID, proposalID) + + // Update vote counts (incremental tallying) + proposal.TotalVotes++ + switch choice { + case state.VoteChoiceYes: + proposal.YesVotes++ + case state.VoteChoiceNo: + proposal.NoVotes++ + case state.VoteChoiceAbstain: + proposal.AbstainVotes++ + } + + // Update proposal + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + + // Emit event + ic.AddNotification(e.Hash, VoteCastEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewBigInteger(big.NewInt(int64(token.TokenID))), + stackitem.NewBigInteger(big.NewInt(int64(choice))), + })) + + return stackitem.NewBool(true) +} + +// tallyVotes finalizes voting after the deadline. +func (e *Eligere) tallyVotes(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + + proposal := e.getProposalInternal(ic.DAO, proposalID) + if proposal == nil { + panic(ErrProposalNotFound) + } + + // Must be after voting period + if ic.Block.Index <= proposal.VotingEndsAt { + panic(ErrVotingPeriodNotEnded) + } + + // Must still be active + if proposal.Status != state.ProposalStatusActive { + panic(ErrProposalAlreadyFinalized) + } + + // Get total eligible voters for quorum calculation + // Use Vita token count as the voter base + totalVoters := e.Vita.GetTotalTokenCount(ic.DAO) + if totalVoters == 0 { + totalVoters = 1 // Avoid division by zero + } + + oldStatus := proposal.Status + + // Check quorum (total votes including abstentions) + quorumRequired := (totalVoters * uint64(proposal.QuorumPercent)) / 100 + if proposal.TotalVotes < quorumRequired { + proposal.Status = state.ProposalStatusExpired + + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) + + ic.AddNotification(e.Hash, ProposalExpiredEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewByteArray([]byte("quorum not met")), + })) + return stackitem.NewBool(false) + } + + // Check threshold (yes votes vs yes+no, abstentions don't count toward threshold) + totalDecisiveVotes := proposal.YesVotes + proposal.NoVotes + var supportPercent uint64 = 0 + if totalDecisiveVotes > 0 { + supportPercent = (proposal.YesVotes * 100) / totalDecisiveVotes + } + + if totalDecisiveVotes == 0 || supportPercent < uint64(proposal.ThresholdPercent) { + proposal.Status = state.ProposalStatusRejected + + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) + + ic.AddNotification(e.Hash, ProposalRejectedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), + })) + return stackitem.NewBool(false) + } + + // Proposal passed + proposal.Status = state.ProposalStatusPassed + + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) + + ic.AddNotification(e.Hash, ProposalPassedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewBigInteger(big.NewInt(int64(supportPercent))), + stackitem.NewBigInteger(big.NewInt(int64(proposal.TotalVotes))), + })) + + return stackitem.NewBool(true) +} + +// executeProposal executes a passed proposal after the execution delay. +func (e *Eligere) executeProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + + caller := ic.VM.GetCallingScriptHash() + + proposal := e.getProposalInternal(ic.DAO, proposalID) + if proposal == nil { + panic(ErrProposalNotFound) + } + + // Must have passed + if proposal.Status != state.ProposalStatusPassed { + panic(ErrProposalNotPassed) + } + + // Check execution delay has passed + executionBlock := proposal.VotingEndsAt + proposal.ExecutionDelay + if ic.Block.Index < executionBlock { + panic(ErrExecutionDelayNotPassed) + } + + // Already executed check + if proposal.ExecutedAt > 0 { + panic(ErrProposalAlreadyExecuted) + } + + oldStatus := proposal.Status + + // Execute based on category + switch proposal.Category { + case state.ProposalCategoryLawAmendment: + // Call Lex.ratifyAmendment if Lex is available + if e.Lex != nil { + e.executeLawAmendment(ic, proposal) + } + case state.ProposalCategoryConstitutional: + // Constitutional amendments also go through Lex + if e.Lex != nil { + e.executeLawAmendment(ic, proposal) + } + case state.ProposalCategoryGovernanceAction: + // Governance actions may call target contract + // Implementation depends on specific action + case state.ProposalCategoryInvestment: + // Investment proposals - future integration + case state.ProposalCategoryReferendum: + // Referendums are advisory, no automatic execution + } + + // Update proposal status + proposal.Status = state.ProposalStatusExecuted + proposal.ExecutedAt = ic.Block.Index + proposal.ExecutedBy = caller + + if err := e.putProposal(ic.DAO, proposal); err != nil { + panic(err) + } + e.updateStatusIndex(ic.DAO, proposalID, oldStatus, proposal.Status) + + // Emit event + ic.AddNotification(e.Hash, ProposalExecutedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(proposalID))), + stackitem.NewByteArray(caller.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(ic.Block.Index))), + })) + + return stackitem.NewBool(true) +} + +// executeLawAmendment calls Lex to ratify a law amendment. +func (e *Eligere) executeLawAmendment(ic *interop.Context, proposal *state.Proposal) { + // The Lex contract's ratifyAmendment method will be called + // This creates or updates a law based on the proposal + if e.Lex != nil { + // All amendments create Federal-level laws + // Constitutional rights (RightLife, RightLiberty, etc.) are immutable in code + // Constitutional proposals require 67% supermajority but still create Federal laws + lawCategory := state.LawCategoryFederal + + // Call Lex to ratify the amendment + e.Lex.RatifyAmendmentInternal(ic, proposal.ID, proposal.ContentHash, lawCategory, 0) + } +} + +// ============ Query Methods ============ + +func (e *Eligere) getProposal(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + + proposal := e.getProposalInternal(ic.DAO, proposalID) + if proposal == nil { + return stackitem.Null{} + } + + item, err := proposal.ToStackItem() + if err != nil { + return stackitem.Null{} + } + return item +} + +func (e *Eligere) hasVoted(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + voter := toUint160(args[1]) + + // Get voter's Vita token + token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) + if err != nil || token == nil { + return stackitem.NewBool(false) + } + + return stackitem.NewBool(e.hasVotedInternal(ic.DAO, proposalID, token.TokenID)) +} + +func (e *Eligere) getVote(ic *interop.Context, args []stackitem.Item) stackitem.Item { + proposalID := toBigInt(args[0]).Uint64() + voter := toUint160(args[1]) + + // Get voter's Vita token + token, err := e.Vita.GetTokenByOwner(ic.DAO, voter) + if err != nil || token == nil { + return stackitem.Null{} + } + + vote := e.getVoteInternal(ic.DAO, proposalID, token.TokenID) + if vote == nil { + return stackitem.Null{} + } + + item, err := vote.ToStackItem() + if err != nil { + return stackitem.Null{} + } + return item +} + +func (e *Eligere) getProposalCount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + key := []byte{eligerePrefixProposalCounter} + count := getIntWithKey(e.ID, ic.DAO, key) + return stackitem.NewBigInteger(big.NewInt(count)) +} + +func (e *Eligere) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cfg := e.getConfigInternal(ic.DAO) + item, err := cfg.ToStackItem() + if err != nil { + return stackitem.Null{} + } + return item +} diff --git a/pkg/core/native/event_archival.go b/pkg/core/native/event_archival.go new file mode 100755 index 0000000..fc785bb --- /dev/null +++ b/pkg/core/native/event_archival.go @@ -0,0 +1,240 @@ +package native + +import ( + "encoding/binary" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "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/util" +) + +// LOW-001: Event archival system for managing event log growth. + +type EventArchivalConfig struct { + RetentionBlocks uint32 + ArchiveEnabled bool + CommitmentInterval uint32 +} + +func DefaultEventArchivalConfig() *EventArchivalConfig { + return &EventArchivalConfig{ + RetentionBlocks: 2592000, + ArchiveEnabled: false, + CommitmentInterval: 8640, + } +} + +type EventArchivalState struct { + LastArchivedBlock uint32 + LastCommitmentBlock uint32 + TotalEventsArchived uint64 + TotalCommitments uint64 +} + +type EventCommitment struct { + CommitmentID uint64 + StartBlock uint32 + EndBlock uint32 + MerkleRoot util.Uint256 + EventCount uint64 + CreatedAt uint32 +} + +const ( + eventArchivalPrefixConfig byte = 0xE0 + eventArchivalPrefixState byte = 0xE1 + eventArchivalPrefixCommitment byte = 0xE2 + eventArchivalPrefixByBlock byte = 0xE3 +) + +type EventArchiver struct { + contractID int32 +} + +func NewEventArchiver(contractID int32) *EventArchiver { + return &EventArchiver{contractID: contractID} +} + +func (ea *EventArchiver) GetConfig(d *dao.Simple) *EventArchivalConfig { + key := []byte{eventArchivalPrefixConfig} + si := d.GetStorageItem(ea.contractID, key) + if si == nil { + return DefaultEventArchivalConfig() + } + cfg := &EventArchivalConfig{} + cfg.RetentionBlocks = binary.BigEndian.Uint32(si[:4]) + cfg.ArchiveEnabled = si[4] == 1 + cfg.CommitmentInterval = binary.BigEndian.Uint32(si[5:9]) + return cfg +} + +func (ea *EventArchiver) PutConfig(d *dao.Simple, cfg *EventArchivalConfig) { + key := []byte{eventArchivalPrefixConfig} + data := make([]byte, 9) + binary.BigEndian.PutUint32(data[:4], cfg.RetentionBlocks) + if cfg.ArchiveEnabled { + data[4] = 1 + } + binary.BigEndian.PutUint32(data[5:9], cfg.CommitmentInterval) + d.PutStorageItem(ea.contractID, key, data) +} + +func (ea *EventArchiver) GetState(d *dao.Simple) *EventArchivalState { + key := []byte{eventArchivalPrefixState} + si := d.GetStorageItem(ea.contractID, key) + if si == nil { + return &EventArchivalState{} + } + st := &EventArchivalState{} + st.LastArchivedBlock = binary.BigEndian.Uint32(si[:4]) + st.LastCommitmentBlock = binary.BigEndian.Uint32(si[4:8]) + st.TotalEventsArchived = binary.BigEndian.Uint64(si[8:16]) + st.TotalCommitments = binary.BigEndian.Uint64(si[16:24]) + return st +} + +func (ea *EventArchiver) PutState(d *dao.Simple, st *EventArchivalState) { + key := []byte{eventArchivalPrefixState} + data := make([]byte, 24) + binary.BigEndian.PutUint32(data[:4], st.LastArchivedBlock) + binary.BigEndian.PutUint32(data[4:8], st.LastCommitmentBlock) + binary.BigEndian.PutUint64(data[8:16], st.TotalEventsArchived) + binary.BigEndian.PutUint64(data[16:24], st.TotalCommitments) + d.PutStorageItem(ea.contractID, key, data) +} + +func (ea *EventArchiver) CreateCommitment(d *dao.Simple, startBlock, endBlock uint32, events [][]byte) *EventCommitment { + st := ea.GetState(d) + merkleRoot := computeEventsMerkleRoot(events) + commitment := &EventCommitment{ + CommitmentID: st.TotalCommitments + 1, + StartBlock: startBlock, + EndBlock: endBlock, + MerkleRoot: merkleRoot, + EventCount: uint64(len(events)), + CreatedAt: endBlock, + } + ea.putCommitment(d, commitment) + st.LastCommitmentBlock = endBlock + st.TotalCommitments++ + st.TotalEventsArchived += uint64(len(events)) + ea.PutState(d, st) + return commitment +} + +func (ea *EventArchiver) GetCommitment(d *dao.Simple, commitmentID uint64) *EventCommitment { + key := make([]byte, 9) + key[0] = eventArchivalPrefixCommitment + binary.BigEndian.PutUint64(key[1:], commitmentID) + si := d.GetStorageItem(ea.contractID, key) + if si == nil { + return nil + } + c := &EventCommitment{} + c.CommitmentID = binary.BigEndian.Uint64(si[:8]) + c.StartBlock = binary.BigEndian.Uint32(si[8:12]) + c.EndBlock = binary.BigEndian.Uint32(si[12:16]) + copy(c.MerkleRoot[:], si[16:48]) + c.EventCount = binary.BigEndian.Uint64(si[48:56]) + c.CreatedAt = binary.BigEndian.Uint32(si[56:60]) + return c +} + +func (ea *EventArchiver) putCommitment(d *dao.Simple, c *EventCommitment) { + key := make([]byte, 9) + key[0] = eventArchivalPrefixCommitment + binary.BigEndian.PutUint64(key[1:], c.CommitmentID) + data := make([]byte, 60) + binary.BigEndian.PutUint64(data[:8], c.CommitmentID) + binary.BigEndian.PutUint32(data[8:12], c.StartBlock) + binary.BigEndian.PutUint32(data[12:16], c.EndBlock) + copy(data[16:48], c.MerkleRoot[:]) + binary.BigEndian.PutUint64(data[48:56], c.EventCount) + binary.BigEndian.PutUint32(data[56:60], c.CreatedAt) + d.PutStorageItem(ea.contractID, key, data) + blockKey := make([]byte, 5) + blockKey[0] = eventArchivalPrefixByBlock + binary.BigEndian.PutUint32(blockKey[1:], c.EndBlock) + binary.BigEndian.PutUint64(data[:8], c.CommitmentID) + d.PutStorageItem(ea.contractID, blockKey, data[:8]) +} + +func (ea *EventArchiver) GetCommitmentForBlock(d *dao.Simple, blockHeight uint32) *EventCommitment { + var foundID uint64 + prefix := []byte{eventArchivalPrefixByBlock} + d.Seek(ea.contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 4 && len(v) >= 8 { + endBlock := binary.BigEndian.Uint32(k[:4]) + if endBlock >= blockHeight { + foundID = binary.BigEndian.Uint64(v[:8]) + return false + } + } + return true + }) + if foundID == 0 { + return nil + } + return ea.GetCommitment(d, foundID) +} + +func (ea *EventArchiver) ShouldCreateCommitment(d *dao.Simple, currentBlock uint32) bool { + cfg := ea.GetConfig(d) + if !cfg.ArchiveEnabled { + return false + } + st := ea.GetState(d) + return currentBlock >= st.LastCommitmentBlock+cfg.CommitmentInterval +} + +func (ea *EventArchiver) CanPruneEvents(d *dao.Simple, blockHeight, currentBlock uint32) bool { + cfg := ea.GetConfig(d) + if !cfg.ArchiveEnabled { + return false + } + if currentBlock < blockHeight+cfg.RetentionBlocks { + return false + } + commitment := ea.GetCommitmentForBlock(d, blockHeight) + return commitment != nil +} + +func computeEventsMerkleRoot(events [][]byte) util.Uint256 { + if len(events) == 0 { + return util.Uint256{} + } + hashes := make([]util.Uint256, len(events)) + for i, event := range events { + hashes[i] = hash.Sha256(event) + } + for len(hashes) > 1 { + if len(hashes)%2 == 1 { + hashes = append(hashes, hashes[len(hashes)-1]) + } + newHashes := make([]util.Uint256, len(hashes)/2) + for i := 0; i < len(hashes); i += 2 { + combined := make([]byte, 64) + copy(combined[:32], hashes[i][:]) + copy(combined[32:], hashes[i+1][:]) + newHashes[i/2] = hash.Sha256(combined) + } + hashes = newHashes + } + return hashes[0] +} + +func VerifyEventInCommitment(eventData []byte, merkleProof []util.Uint256, index int, merkleRoot util.Uint256) bool { + h := hash.Sha256(eventData) + for _, proofElement := range merkleProof { + var combined []byte + if index%2 == 0 { + combined = append(h[:], proofElement[:]...) + } else { + combined = append(proofElement[:], h[:]...) + } + h = hash.Sha256(combined) + index /= 2 + } + return h == merkleRoot +} diff --git a/pkg/core/native/federation.go b/pkg/core/native/federation.go old mode 100644 new mode 100755 diff --git a/pkg/core/native/invariants.go b/pkg/core/native/invariants.go new file mode 100644 index 0000000..3c5b5b2 --- /dev/null +++ b/pkg/core/native/invariants.go @@ -0,0 +1,528 @@ +package native + +import ( + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/core/storage" + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// ARCH-003: Formal Verification Invariants +// This file documents critical system invariants that must hold at all times. +// These invariants serve multiple purposes: +// 1. Runtime verification during testing and canary deployments +// 2. Documentation for formal verification tools (TLA+, Coq, etc.) +// 3. Post-deployment monitoring and alerting + +// InvariantCategory categorizes invariants by domain. +type InvariantCategory uint8 + +const ( + InvariantCategoryToken InvariantCategory = iota + InvariantCategoryIdentity + InvariantCategoryGovernance + InvariantCategoryEconomic + InvariantCategorySecurity + InvariantCategoryCrossContract +) + +// InvariantSeverity indicates the impact of violation. +type InvariantSeverity uint8 + +const ( + // InvariantSeverityCritical means system halt required + InvariantSeverityCritical InvariantSeverity = iota + // InvariantSeverityHigh means immediate investigation required + InvariantSeverityHigh + // InvariantSeverityMedium means potential issue to investigate + InvariantSeverityMedium + // InvariantSeverityLow means informational anomaly + InvariantSeverityLow +) + +// Invariant represents a system invariant that must hold. +type Invariant struct { + ID string + Name string + Description string + Category InvariantCategory + Severity InvariantSeverity + // FormalSpec is TLA+/Coq-style formal specification + FormalSpec string +} + +// InvariantViolation records a violation of a system invariant. +type InvariantViolation struct { + InvariantID string + BlockHeight uint32 + Details string + ActualValue string + ExpectedSpec string +} + +// Invariant violations are critical errors. +var ( + ErrInvariantViolation = errors.New("invariant violation detected") + ErrTokenSupplyMismatch = errors.New("token supply invariant violated") + ErrVitaUniqueness = errors.New("vita uniqueness invariant violated") + ErrRightsConsistency = errors.New("rights consistency invariant violated") + ErrBalanceNonNegative = errors.New("balance non-negativity invariant violated") + ErrCrossContractConsistency = errors.New("cross-contract consistency invariant violated") +) + +// CriticalInvariants defines all system invariants that must always hold. +var CriticalInvariants = []Invariant{ + // ============================================ + // TOKEN INVARIANTS + // ============================================ + { + ID: "TOK-001", + Name: "VTS Total Supply Conservation", + Description: "Sum of all VTS balances must equal total supply minus burned amount", + Category: InvariantCategoryToken, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT VTSSupplyConservation == + \A state \in ValidStates: + Sum({state.vts.balance[addr] : addr \in DOMAIN state.vts.balance}) + = state.vts.totalSupply - state.vts.burnedAmount + `, + }, + { + ID: "TOK-002", + Name: "Non-Negative Balances", + Description: "No token balance can ever be negative", + Category: InvariantCategoryToken, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT NonNegativeBalances == + \A state \in ValidStates: + \A addr \in DOMAIN state.vts.balance: + state.vts.balance[addr] >= 0 + `, + }, + { + ID: "TOK-003", + Name: "Lub Total Supply Conservation", + Description: "Lub supply follows predictable generation schedule", + Category: InvariantCategoryToken, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT LubSupplyConservation == + \A state \in ValidStates: + state.lub.totalSupply <= MaxLubSupply + /\ Sum({state.lub.balance[addr] : addr \in DOMAIN state.lub.balance}) + = state.lub.totalSupply + `, + }, + + // ============================================ + // IDENTITY INVARIANTS + // ============================================ + { + ID: "IDN-001", + Name: "Vita Uniqueness Per Person", + Description: "Each natural person can hold at most one Vita token globally", + Category: InvariantCategoryIdentity, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT VitaUniqueness == + \A state \in ValidStates: + \A p1, p2 \in state.vita.tokens: + p1.biometricHash = p2.biometricHash => p1.tokenID = p2.tokenID + `, + }, + { + ID: "IDN-002", + Name: "Vita Non-Transferability", + Description: "Vita tokens cannot be transferred except through death/recovery", + Category: InvariantCategoryIdentity, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT VitaNonTransferable == + \A state, state' \in ValidStates: + \A vita \in state.vita.tokens: + state'.vita.tokens[vita.tokenID].owner # vita.owner + => state'.vita.tokens[vita.tokenID].status \in {Suspended, Revoked} + \/ vita.owner.status = Deceased + `, + }, + { + ID: "IDN-003", + Name: "Vita Count Equals Active Citizens", + Description: "Active Vita count must match registered citizen count", + Category: InvariantCategoryIdentity, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT VitaCountConsistency == + \A state \in ValidStates: + Cardinality({v \in state.vita.tokens : v.status = Active}) + = state.vita.activeCount + `, + }, + + // ============================================ + // GOVERNANCE INVARIANTS + // ============================================ + { + ID: "GOV-001", + Name: "One Person One Vote", + Description: "Each Vita holder can cast at most one vote per proposal", + Category: InvariantCategoryGovernance, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT OnePersonOneVote == + \A state \in ValidStates: + \A p \in state.eligere.proposals: + \A v \in state.vita.tokens: + Cardinality({vote \in p.votes : vote.vitaID = v.tokenID}) <= 1 + `, + }, + { + ID: "GOV-002", + Name: "Voting Age Enforcement", + Description: "Only Vita holders >= voting age can vote", + Category: InvariantCategoryGovernance, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT VotingAgeEnforced == + \A state \in ValidStates: + \A vote \in AllVotes(state): + AgeOf(vote.vitaID, state.currentBlock) >= state.config.votingAge + `, + }, + { + ID: "GOV-003", + Name: "Committee Authority Separation", + Description: "Domain committees have authority only within their domain", + Category: InvariantCategoryGovernance, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT CommitteeAuthoritySeparation == + \A state \in ValidStates: + \A action \in state.actions: + action.requiredDomain = DomainHealth + => action.authorizer \in state.committees.health + `, + }, + + // ============================================ + // ECONOMIC INVARIANTS + // ============================================ + { + ID: "ECO-001", + Name: "Tribute Conservation", + Description: "Total tribute collected equals sum of redistributions plus treasury", + Category: InvariantCategoryEconomic, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT TributeConservation == + \A state \in ValidStates: + state.tribute.totalCollected + = state.tribute.totalRedistributed + state.treasury.tributeBalance + `, + }, + { + ID: "ECO-002", + Name: "Investment Opportunity Bounds", + Description: "Investment totals cannot exceed opportunity limits", + Category: InvariantCategoryEconomic, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT InvestmentBounds == + \A state \in ValidStates: + \A opp \in state.collocatio.opportunities: + Sum({inv.amount : inv \in opp.investments}) <= opp.maxAmount + `, + }, + + // ============================================ + // SECURITY INVARIANTS + // ============================================ + { + ID: "SEC-001", + Name: "Rights Restriction Requires Due Process", + Description: "Rights can only be restricted with valid judicial order", + Category: InvariantCategorySecurity, + Severity: InvariantSeverityCritical, + FormalSpec: ` + INVARIANT RightsRestrictionDueProcess == + \A state \in ValidStates: + \A restriction \in state.lex.restrictions: + restriction.caseID # "" + /\ restriction.authorizedBy \in state.roles.judges + /\ restriction.expiresAt > state.currentBlock + `, + }, + { + ID: "SEC-002", + Name: "Role Expiry Enforcement", + Description: "Expired roles grant no permissions", + Category: InvariantCategorySecurity, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT ExpiredRolesInactive == + \A state \in ValidStates: + \A role \in state.roleRegistry.assignments: + role.expiresAt <= state.currentBlock + => ~HasPermission(role.holder, role.roleID, state) + `, + }, + { + ID: "SEC-003", + Name: "Circuit Breaker Effectiveness", + Description: "Open circuit breakers block all protected operations", + Category: InvariantCategorySecurity, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT CircuitBreakerEffective == + \A state \in ValidStates: + \A breaker \in state.circuitBreakers: + breaker.state = Open + => ~\E op \in state.operations: + op.protected = breaker.name /\ op.status = Completed + `, + }, + + // ============================================ + // CROSS-CONTRACT INVARIANTS + // ============================================ + { + ID: "XCT-001", + Name: "Annos-Vita Consistency", + Description: "Every Annos lifespan record has corresponding Vita token", + Category: InvariantCategoryCrossContract, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT AnnosVitaConsistency == + \A state \in ValidStates: + \A record \in state.annos.lifespans: + \E vita \in state.vita.tokens: + vita.tokenID = record.vitaID + `, + }, + { + ID: "XCT-002", + Name: "Scire-Vita Consistency", + Description: "Education accounts require valid Vita holders", + Category: InvariantCategoryCrossContract, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT ScireVitaConsistency == + \A state \in ValidStates: + \A account \in state.scire.accounts: + \E vita \in state.vita.tokens: + vita.tokenID = account.vitaID /\ vita.status = Active + `, + }, + { + ID: "XCT-003", + Name: "Salus-Vita Consistency", + Description: "Healthcare accounts require valid Vita holders", + Category: InvariantCategoryCrossContract, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT SalusVitaConsistency == + \A state \in ValidStates: + \A account \in state.salus.accounts: + \E vita \in state.vita.tokens: + vita.tokenID = account.vitaID + `, + }, + { + ID: "XCT-004", + Name: "Federation Debt Balance", + Description: "Inter-chain debts sum to zero globally", + Category: InvariantCategoryCrossContract, + Severity: InvariantSeverityHigh, + FormalSpec: ` + INVARIANT FederationDebtBalance == + \A globalState \in ValidGlobalStates: + Sum({chain.federation.debt[other] : + chain \in globalState.chains, + other \in globalState.chains}) + = 0 + `, + }, +} + +// InvariantChecker provides runtime invariant verification. +type InvariantChecker struct { + contractID int32 + violations []InvariantViolation +} + +// NewInvariantChecker creates a new invariant checker. +func NewInvariantChecker(contractID int32) *InvariantChecker { + return &InvariantChecker{ + contractID: contractID, + violations: make([]InvariantViolation, 0), + } +} + +// CheckVTSSupplyInvariant verifies VTS supply conservation (TOK-001). +func (ic *InvariantChecker) CheckVTSSupplyInvariant(d *dao.Simple, vtsContractID int32) error { + // Get total supply + supplyKey := []byte{0x11} // VTS total supply prefix + supplyData := d.GetStorageItem(vtsContractID, supplyKey) + if supplyData == nil { + return nil // Not initialized + } + totalSupply := new(big.Int).SetBytes(supplyData) + + // Sum all balances + balanceSum := big.NewInt(0) + balancePrefix := []byte{0x14} // VTS balance prefix + + d.Seek(vtsContractID, storage.SeekRange{Prefix: balancePrefix}, func(k, v []byte) bool { + balance := new(big.Int).SetBytes(v) + balanceSum.Add(balanceSum, balance) + return true + }) + + if totalSupply.Cmp(balanceSum) != 0 { + ic.recordViolation("TOK-001", 0, "Supply mismatch", + balanceSum.String(), totalSupply.String()) + return ErrTokenSupplyMismatch + } + + return nil +} + +// CheckVitaUniquenessInvariant verifies Vita uniqueness (IDN-001). +func (ic *InvariantChecker) CheckVitaUniquenessInvariant(d *dao.Simple, vitaContractID int32) error { + biometricHashes := make(map[util.Uint256]uint64) + tokenPrefix := []byte{0x01} // Vita token prefix + + var violation bool + d.Seek(vitaContractID, storage.SeekRange{Prefix: tokenPrefix}, func(k, v []byte) bool { + if len(v) < 32 { + return true + } + // Extract biometric hash from token data + var bioHash util.Uint256 + copy(bioHash[:], v[:32]) + + if existingID, exists := biometricHashes[bioHash]; exists { + ic.recordViolation("IDN-001", 0, "Duplicate biometric hash", + "Multiple tokens", "Single token per biometric") + _ = existingID // Reference to suppress unused warning + violation = true + return false + } + + // Extract token ID from key + if len(k) >= 8 { + tokenID := uint64(k[0])<<56 | uint64(k[1])<<48 | uint64(k[2])<<40 | + uint64(k[3])<<32 | uint64(k[4])<<24 | uint64(k[5])<<16 | + uint64(k[6])<<8 | uint64(k[7]) + biometricHashes[bioHash] = tokenID + } + return true + }) + + if violation { + return ErrVitaUniqueness + } + return nil +} + +// CheckNonNegativeBalances verifies no negative balances exist (TOK-002). +func (ic *InvariantChecker) CheckNonNegativeBalances(d *dao.Simple, contractID int32, balancePrefix byte) error { + prefix := []byte{balancePrefix} + + var violation bool + d.Seek(contractID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + balance := new(big.Int).SetBytes(v) + if balance.Sign() < 0 { + ic.recordViolation("TOK-002", 0, "Negative balance detected", + balance.String(), ">= 0") + violation = true + return false + } + return true + }) + + if violation { + return ErrBalanceNonNegative + } + return nil +} + +// CheckRightsConsistency verifies rights restrictions have valid due process (SEC-001). +func (ic *InvariantChecker) CheckRightsConsistency(d *dao.Simple, lexContractID int32, currentBlock uint32) error { + restrictionPrefix := []byte{0x20} // Lex restriction prefix + + var violation bool + d.Seek(lexContractID, storage.SeekRange{Prefix: restrictionPrefix}, func(k, v []byte) bool { + if len(v) < 40 { + return true + } + + // Check if restriction has case ID (non-empty) + caseIDLen := v[0] + if caseIDLen == 0 { + ic.recordViolation("SEC-001", currentBlock, + "Rights restriction without case ID", "empty", "non-empty case ID") + violation = true + return false + } + + return true + }) + + if violation { + return ErrRightsConsistency + } + return nil +} + +// recordViolation records an invariant violation. +func (ic *InvariantChecker) recordViolation(invariantID string, blockHeight uint32, details, actual, expected string) { + ic.violations = append(ic.violations, InvariantViolation{ + InvariantID: invariantID, + BlockHeight: blockHeight, + Details: details, + ActualValue: actual, + ExpectedSpec: expected, + }) +} + +// GetViolations returns all recorded violations. +func (ic *InvariantChecker) GetViolations() []InvariantViolation { + return ic.violations +} + +// ClearViolations clears recorded violations. +func (ic *InvariantChecker) ClearViolations() { + ic.violations = make([]InvariantViolation, 0) +} + +// RunAllCriticalChecks runs all critical invariant checks. +func (ic *InvariantChecker) RunAllCriticalChecks(d *dao.Simple, contracts ContractIDs, currentBlock uint32) []InvariantViolation { + ic.ClearViolations() + + // Run critical checks + _ = ic.CheckVTSSupplyInvariant(d, contracts.VTS) + _ = ic.CheckVitaUniquenessInvariant(d, contracts.Vita) + _ = ic.CheckNonNegativeBalances(d, contracts.VTS, 0x14) + _ = ic.CheckNonNegativeBalances(d, contracts.Tutus, 0x14) + _ = ic.CheckNonNegativeBalances(d, contracts.Lub, 0x14) + _ = ic.CheckRightsConsistency(d, contracts.Lex, currentBlock) + + return ic.GetViolations() +} + +// ContractIDs holds contract identifiers for invariant checking. +type ContractIDs struct { + VTS int32 + Vita int32 + Tutus int32 + Lub int32 + Lex int32 + Annos int32 + Scire int32 + Salus int32 +} diff --git a/pkg/core/native/lex.go b/pkg/core/native/lex.go old mode 100644 new mode 100755 index 464e286..3874cda --- a/pkg/core/native/lex.go +++ b/pkg/core/native/lex.go @@ -85,7 +85,6 @@ var ( ErrRestrictionNotFound = errors.New("restriction not found") ErrRestrictionExists = errors.New("restriction already exists") ErrInvalidRightID = errors.New("invalid right ID") - ErrReasonTooLong = errors.New("reason too long") ErrNoCaseID = errors.New("case ID required for due process") ErrNoExpiration = errors.New("expiration required (no indefinite restrictions)") ErrNotAuthorized = errors.New("not authorized") diff --git a/pkg/core/native/palam.go b/pkg/core/native/palam.go old mode 100644 new mode 100755 index 0fd5fb9..186439e --- a/pkg/core/native/palam.go +++ b/pkg/core/native/palam.go @@ -1,1127 +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 - 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() -} +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() +} diff --git a/pkg/core/native/pons.go b/pkg/core/native/pons.go old mode 100644 new mode 100755 index a42cf88..db650ca --- a/pkg/core/native/pons.go +++ b/pkg/core/native/pons.go @@ -1,1260 +1,1260 @@ -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/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" -) - -// Pons ("bridge" in Latin) represents the Inter-Government Bridge Protocol -// native contract. It manages: -// - Bilateral agreements between sovereign chains -// - Cross-border verification requests -// - International VTS settlement -// - Education and healthcare credential portability -type Pons struct { - interop.ContractMD - Tutus ITutus - Vita IVita - Federation *Federation - RoleRegistry *RoleRegistry - VTS *VTS - Scire *Scire - Salus *Salus -} - -// Storage key prefixes for Pons. -const ( - ponsPrefixConfig byte = 0x01 // -> PonsConfig - ponsPrefixAgreement byte = 0x10 // agreementID -> BilateralAgreement - ponsPrefixAgreementByChain byte = 0x11 // chainID + agreementID -> exists - ponsPrefixAgreementCounter byte = 0x1F // -> next agreementID - ponsPrefixVerification byte = 0x20 // requestID -> VerificationRequest - ponsPrefixVerifBySubject byte = 0x21 // subject + requestID -> exists - ponsPrefixVerifCounter byte = 0x2F // -> next verificationID - ponsPrefixSettlement byte = 0x30 // settlementID -> SettlementRequest - ponsPrefixSettlByChain byte = 0x31 // chainID + settlementID -> exists - ponsPrefixSettlCounter byte = 0x3F // -> next settlementID - ponsPrefixCredential byte = 0x40 // credentialID -> CredentialShare - ponsPrefixCredByOwner byte = 0x41 // owner + credentialID -> exists - ponsPrefixCredCounter byte = 0x4F // -> next credentialID -) - -// Default configuration values. -const ( - defaultLocalChainID uint32 = 1 - defaultVerificationTimeout uint32 = 8640 // ~1 day at 10s blocks - defaultSettlementTimeout uint32 = 86400 // ~10 days - defaultMaxPendingRequests uint64 = 10000 - defaultCredentialShareExpiry uint32 = 315360 // ~1 year -) - -// Event names for Pons. -const ( - AgreementCreatedEvent = "AgreementCreated" - AgreementUpdatedEvent = "AgreementUpdated" - AgreementTerminatedEvent = "AgreementTerminated" - VerificationRequestedEvent = "VerificationRequested" - VerificationRespondedEvent = "VerificationResponded" - SettlementRequestedEvent = "SettlementRequested" - SettlementCompletedEvent = "SettlementCompleted" - CredentialSharedEvent = "CredentialShared" - CredentialRevokedEvent = "CredentialRevoked" -) - -// Various errors for Pons. -var ( - ErrNoAgreement = errors.New("no active agreement with target chain") - ErrAgreementExists = errors.New("agreement already exists") - ErrAgreementNotFound = errors.New("agreement not found") - ErrInvalidAgreementType = errors.New("invalid agreement type") - ErrAgreementNotActive = errors.New("agreement not active") - ErrVerificationNotFound = errors.New("verification request not found") - ErrVerificationExpired = errors.New("verification request expired") - ErrSettlementNotFound = errors.New("settlement request not found") - ErrSettlementExpired = errors.New("settlement request expired") - ErrCredentialNotFound = errors.New("credential share not found") - ErrCredentialExpired = errors.New("credential share expired") - ErrCredentialRevoked = errors.New("credential share revoked") - ErrMaxRequestsReached = errors.New("maximum pending requests reached") - ErrNotCredentialOwner = errors.New("not credential owner") - ErrAgreementTypeNotAllowed = errors.New("agreement type not allowed for this operation") -) - -var _ interop.Contract = (*Pons)(nil) - -// newPons creates a new Pons native contract. -func newPons() *Pons { - p := &Pons{ - ContractMD: *interop.NewContractMD(nativenames.Pons, nativeids.Pons), - } - defer p.BuildHFSpecificMD(p.ActiveIn()) - - // getConfig method - desc := NewDescriptor("getConfig", smartcontract.ArrayType) - md := NewMethodAndPrice(p.getConfig, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // setLocalChainID method (committee only) - desc = NewDescriptor("setLocalChainID", smartcontract.BoolType, - manifest.NewParameter("chainID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.setLocalChainID, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // --- Agreement Management --- - - // createAgreement method (committee only) - desc = NewDescriptor("createAgreement", smartcontract.IntegerType, - manifest.NewParameter("remoteChainID", smartcontract.IntegerType), - manifest.NewParameter("agreementType", smartcontract.IntegerType), - manifest.NewParameter("termsHash", smartcontract.Hash256Type), - manifest.NewParameter("expirationHeight", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.createAgreement, 1<<17, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // updateAgreementStatus method (committee only) - desc = NewDescriptor("updateAgreementStatus", smartcontract.BoolType, - manifest.NewParameter("agreementID", smartcontract.IntegerType), - manifest.NewParameter("newStatus", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.updateAgreementStatus, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // getAgreement method - desc = NewDescriptor("getAgreement", smartcontract.ArrayType, - manifest.NewParameter("agreementID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.getAgreement, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // hasActiveAgreement method - desc = NewDescriptor("hasActiveAgreement", smartcontract.BoolType, - manifest.NewParameter("remoteChainID", smartcontract.IntegerType), - manifest.NewParameter("agreementType", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.hasActiveAgreement, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // --- Verification Requests --- - - // requestVerification method - desc = NewDescriptor("requestVerification", smartcontract.IntegerType, - manifest.NewParameter("targetChainID", smartcontract.IntegerType), - manifest.NewParameter("subject", smartcontract.Hash160Type), - manifest.NewParameter("verificationType", smartcontract.IntegerType), - manifest.NewParameter("dataHash", smartcontract.Hash256Type)) - md = NewMethodAndPrice(p.requestVerification, 1<<17, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // respondVerification method (committee only - represents response from other chain) - desc = NewDescriptor("respondVerification", smartcontract.BoolType, - manifest.NewParameter("requestID", smartcontract.IntegerType), - manifest.NewParameter("approved", smartcontract.BoolType), - manifest.NewParameter("responseHash", smartcontract.Hash256Type)) - md = NewMethodAndPrice(p.respondVerification, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // getVerificationRequest method - desc = NewDescriptor("getVerificationRequest", smartcontract.ArrayType, - manifest.NewParameter("requestID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.getVerificationRequest, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // --- Settlement Requests --- - - // requestSettlement method - desc = NewDescriptor("requestSettlement", smartcontract.IntegerType, - manifest.NewParameter("toChainID", smartcontract.IntegerType), - manifest.NewParameter("receiver", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("reference", smartcontract.StringType)) - md = NewMethodAndPrice(p.requestSettlement, 1<<17, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // completeSettlement method (committee only - represents confirmation from other chain) - desc = NewDescriptor("completeSettlement", smartcontract.BoolType, - manifest.NewParameter("settlementID", smartcontract.IntegerType), - manifest.NewParameter("txHash", smartcontract.Hash256Type)) - md = NewMethodAndPrice(p.completeSettlement, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // cancelSettlement method (sender or committee) - desc = NewDescriptor("cancelSettlement", smartcontract.BoolType, - manifest.NewParameter("settlementID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.cancelSettlement, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // getSettlementRequest method - desc = NewDescriptor("getSettlementRequest", smartcontract.ArrayType, - manifest.NewParameter("settlementID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.getSettlementRequest, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // --- Credential Sharing --- - - // shareCredential method - desc = NewDescriptor("shareCredential", smartcontract.IntegerType, - manifest.NewParameter("targetChainID", smartcontract.IntegerType), - manifest.NewParameter("credentialType", smartcontract.IntegerType), - manifest.NewParameter("credentialID", smartcontract.IntegerType), - manifest.NewParameter("contentHash", smartcontract.Hash256Type), - manifest.NewParameter("validUntil", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.shareCredential, 1<<17, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // revokeCredentialShare method - desc = NewDescriptor("revokeCredentialShare", smartcontract.BoolType, - manifest.NewParameter("shareID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.revokeCredentialShare, 1<<16, callflag.States|callflag.AllowNotify) - p.AddMethod(md, desc) - - // getCredentialShare method - desc = NewDescriptor("getCredentialShare", smartcontract.ArrayType, - manifest.NewParameter("shareID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.getCredentialShare, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // verifyCredentialShare method - desc = NewDescriptor("verifyCredentialShare", smartcontract.BoolType, - manifest.NewParameter("shareID", smartcontract.IntegerType)) - md = NewMethodAndPrice(p.verifyCredentialShare, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // --- Counter Query Methods --- - - // getAgreementCount method - desc = NewDescriptor("getAgreementCount", smartcontract.IntegerType) - md = NewMethodAndPrice(p.getAgreementCount, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // getVerificationCount method - desc = NewDescriptor("getVerificationCount", smartcontract.IntegerType) - md = NewMethodAndPrice(p.getVerificationCount, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // getSettlementCount method - desc = NewDescriptor("getSettlementCount", smartcontract.IntegerType) - md = NewMethodAndPrice(p.getSettlementCount, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // getCredentialShareCount method - desc = NewDescriptor("getCredentialShareCount", smartcontract.IntegerType) - md = NewMethodAndPrice(p.getCredentialShareCount, 1<<15, callflag.ReadStates) - p.AddMethod(md, desc) - - // --- Events --- - - eDesc := NewEventDescriptor(AgreementCreatedEvent, - manifest.NewParameter("agreementID", smartcontract.IntegerType), - manifest.NewParameter("remoteChainID", smartcontract.IntegerType), - manifest.NewParameter("agreementType", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(AgreementUpdatedEvent, - manifest.NewParameter("agreementID", smartcontract.IntegerType), - manifest.NewParameter("oldStatus", smartcontract.IntegerType), - manifest.NewParameter("newStatus", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(AgreementTerminatedEvent, - manifest.NewParameter("agreementID", smartcontract.IntegerType), - manifest.NewParameter("remoteChainID", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(VerificationRequestedEvent, - manifest.NewParameter("requestID", smartcontract.IntegerType), - manifest.NewParameter("targetChainID", smartcontract.IntegerType), - manifest.NewParameter("subject", smartcontract.Hash160Type), - manifest.NewParameter("verificationType", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(VerificationRespondedEvent, - manifest.NewParameter("requestID", smartcontract.IntegerType), - manifest.NewParameter("approved", smartcontract.BoolType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(SettlementRequestedEvent, - manifest.NewParameter("settlementID", smartcontract.IntegerType), - manifest.NewParameter("toChainID", smartcontract.IntegerType), - manifest.NewParameter("receiver", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(SettlementCompletedEvent, - manifest.NewParameter("settlementID", smartcontract.IntegerType), - manifest.NewParameter("txHash", smartcontract.Hash256Type)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(CredentialSharedEvent, - manifest.NewParameter("shareID", smartcontract.IntegerType), - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("targetChainID", smartcontract.IntegerType), - manifest.NewParameter("credentialType", smartcontract.IntegerType)) - p.AddEvent(NewEvent(eDesc)) - - eDesc = NewEventDescriptor(CredentialRevokedEvent, - manifest.NewParameter("shareID", smartcontract.IntegerType), - manifest.NewParameter("owner", smartcontract.Hash160Type)) - p.AddEvent(NewEvent(eDesc)) - - return p -} - -// Metadata returns contract metadata. -func (p *Pons) Metadata() *interop.ContractMD { - return &p.ContractMD -} - -// Initialize initializes Pons contract at the specified hardfork. -func (p *Pons) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != p.ActiveIn() { - return nil - } - - // Initialize default config - cfg := state.PonsConfig{ - LocalChainID: defaultLocalChainID, - VerificationTimeout: defaultVerificationTimeout, - SettlementTimeout: defaultSettlementTimeout, - MaxPendingRequests: defaultMaxPendingRequests, - CredentialShareExpiry: defaultCredentialShareExpiry, - } - p.setConfigInternal(ic.DAO, &cfg) - - return nil -} - -// InitializeCache fills native Pons cache from DAO on node restart. -func (p *Pons) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - return nil -} - -// OnPersist implements the Contract interface. -func (p *Pons) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist implements the Contract interface. -func (p *Pons) PostPersist(ic *interop.Context) error { - return nil -} - -// ActiveIn returns the hardfork this contract activates in (nil = always active). -func (p *Pons) ActiveIn() *config.Hardfork { - return nil -} - -// ============================================================================ -// Storage Key Helpers -// ============================================================================ - -func makePonsConfigKey() []byte { - return []byte{ponsPrefixConfig} -} - -func makePonsAgreementKey(agreementID uint64) []byte { - key := make([]byte, 9) - key[0] = ponsPrefixAgreement - binary.BigEndian.PutUint64(key[1:], agreementID) - return key -} - -func makePonsAgreementByChainKey(chainID uint32, agreementID uint64) []byte { - key := make([]byte, 13) - key[0] = ponsPrefixAgreementByChain - binary.BigEndian.PutUint32(key[1:], chainID) - binary.BigEndian.PutUint64(key[5:], agreementID) - return key -} - -func makePonsAgreementCounterKey() []byte { - return []byte{ponsPrefixAgreementCounter} -} - -func makePonsVerificationKey(requestID uint64) []byte { - key := make([]byte, 9) - key[0] = ponsPrefixVerification - binary.BigEndian.PutUint64(key[1:], requestID) - return key -} - -func makePonsVerifBySubjectKey(subject util.Uint160, requestID uint64) []byte { - key := make([]byte, 1+util.Uint160Size+8) - key[0] = ponsPrefixVerifBySubject - copy(key[1:], subject.BytesBE()) - binary.BigEndian.PutUint64(key[1+util.Uint160Size:], requestID) - return key -} - -func makePonsVerificationCounterKey() []byte { - return []byte{ponsPrefixVerifCounter} -} - -func makePonsSettlementKey(settlementID uint64) []byte { - key := make([]byte, 9) - key[0] = ponsPrefixSettlement - binary.BigEndian.PutUint64(key[1:], settlementID) - return key -} - -func makePonsSettlByChainKey(chainID uint32, settlementID uint64) []byte { - key := make([]byte, 13) - key[0] = ponsPrefixSettlByChain - binary.BigEndian.PutUint32(key[1:], chainID) - binary.BigEndian.PutUint64(key[5:], settlementID) - return key -} - -func makePonsSettlementCounterKey() []byte { - return []byte{ponsPrefixSettlCounter} -} - -func makePonsCredentialKey(credentialID uint64) []byte { - key := make([]byte, 9) - key[0] = ponsPrefixCredential - binary.BigEndian.PutUint64(key[1:], credentialID) - return key -} - -func makePonsCredByOwnerKey(owner util.Uint160, credentialID uint64) []byte { - key := make([]byte, 1+util.Uint160Size+8) - key[0] = ponsPrefixCredByOwner - copy(key[1:], owner.BytesBE()) - binary.BigEndian.PutUint64(key[1+util.Uint160Size:], credentialID) - return key -} - -func makePonsCredentialCounterKey() []byte { - return []byte{ponsPrefixCredCounter} -} - -// ============================================================================ -// Internal Storage Methods -// ============================================================================ - -func (p *Pons) getConfigInternal(d *dao.Simple) *state.PonsConfig { - si := d.GetStorageItem(p.ID, makePonsConfigKey()) - if si == nil { - return &state.PonsConfig{ - LocalChainID: defaultLocalChainID, - VerificationTimeout: defaultVerificationTimeout, - SettlementTimeout: defaultSettlementTimeout, - MaxPendingRequests: defaultMaxPendingRequests, - CredentialShareExpiry: defaultCredentialShareExpiry, - } - } - // Decode config: chainID(4) + verifTimeout(4) + settlTimeout(4) + maxReq(8) + credExpiry(4) = 24 bytes - if len(si) < 24 { - return &state.PonsConfig{ - LocalChainID: defaultLocalChainID, - VerificationTimeout: defaultVerificationTimeout, - SettlementTimeout: defaultSettlementTimeout, - MaxPendingRequests: defaultMaxPendingRequests, - CredentialShareExpiry: defaultCredentialShareExpiry, - } - } - return &state.PonsConfig{ - LocalChainID: binary.BigEndian.Uint32(si[0:4]), - VerificationTimeout: binary.BigEndian.Uint32(si[4:8]), - SettlementTimeout: binary.BigEndian.Uint32(si[8:12]), - MaxPendingRequests: binary.BigEndian.Uint64(si[12:20]), - CredentialShareExpiry: binary.BigEndian.Uint32(si[20:24]), - } -} - -func (p *Pons) setConfigInternal(d *dao.Simple, cfg *state.PonsConfig) { - buf := make([]byte, 24) - binary.BigEndian.PutUint32(buf[0:4], cfg.LocalChainID) - binary.BigEndian.PutUint32(buf[4:8], cfg.VerificationTimeout) - binary.BigEndian.PutUint32(buf[8:12], cfg.SettlementTimeout) - binary.BigEndian.PutUint64(buf[12:20], cfg.MaxPendingRequests) - binary.BigEndian.PutUint32(buf[20:24], cfg.CredentialShareExpiry) - d.PutStorageItem(p.ID, makePonsConfigKey(), buf) -} - -func (p *Pons) getCounterInternal(d *dao.Simple, key []byte) uint64 { - si := d.GetStorageItem(p.ID, key) - if si == nil || len(si) < 8 { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (p *Pons) incrementCounterInternal(d *dao.Simple, key []byte) uint64 { - current := p.getCounterInternal(d, key) - next := current + 1 - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, next) - d.PutStorageItem(p.ID, key, buf) - return next -} - -// Agreement storage format: -// localChainID(4) + remoteChainID(4) + agreementType(1) + status(1) + terms(32) + -// effectiveDate(4) + expirationDate(4) + createdAt(4) + updatedAt(4) = 58 bytes -func (p *Pons) getAgreementInternal(d *dao.Simple, agreementID uint64) (*state.BilateralAgreement, bool) { - si := d.GetStorageItem(p.ID, makePonsAgreementKey(agreementID)) - if si == nil || len(si) < 58 { - return nil, false - } - - var terms util.Uint256 - copy(terms[:], si[10:42]) - - return &state.BilateralAgreement{ - ID: agreementID, - LocalChainID: binary.BigEndian.Uint32(si[0:4]), - RemoteChainID: binary.BigEndian.Uint32(si[4:8]), - AgreementType: state.AgreementType(si[8]), - Status: state.AgreementStatus(si[9]), - Terms: terms, - EffectiveDate: binary.BigEndian.Uint32(si[42:46]), - ExpirationDate: binary.BigEndian.Uint32(si[46:50]), - CreatedAt: binary.BigEndian.Uint32(si[50:54]), - UpdatedAt: binary.BigEndian.Uint32(si[54:58]), - }, true -} - -func (p *Pons) setAgreementInternal(d *dao.Simple, agr *state.BilateralAgreement) { - buf := make([]byte, 58) - binary.BigEndian.PutUint32(buf[0:4], agr.LocalChainID) - binary.BigEndian.PutUint32(buf[4:8], agr.RemoteChainID) - buf[8] = byte(agr.AgreementType) - buf[9] = byte(agr.Status) - copy(buf[10:42], agr.Terms[:]) - binary.BigEndian.PutUint32(buf[42:46], agr.EffectiveDate) - binary.BigEndian.PutUint32(buf[46:50], agr.ExpirationDate) - binary.BigEndian.PutUint32(buf[50:54], agr.CreatedAt) - binary.BigEndian.PutUint32(buf[54:58], agr.UpdatedAt) - d.PutStorageItem(p.ID, makePonsAgreementKey(agr.ID), buf) - // Index by chain - d.PutStorageItem(p.ID, makePonsAgreementByChainKey(agr.RemoteChainID, agr.ID), []byte{1}) -} - -// Verification storage format: -// requestingChain(4) + targetChain(4) + subject(20) + verificationType(1) + dataHash(32) + -// status(1) + responseHash(32) + requester(20) + createdAt(4) + expiresAt(4) + respondedAt(4) = 126 bytes -func (p *Pons) getVerificationInternal(d *dao.Simple, requestID uint64) (*state.VerificationRequest, bool) { - si := d.GetStorageItem(p.ID, makePonsVerificationKey(requestID)) - if si == nil || len(si) < 126 { - return nil, false - } - - var subject util.Uint160 - copy(subject[:], si[8:28]) - var dataHash util.Uint256 - copy(dataHash[:], si[29:61]) - var responseHash util.Uint256 - copy(responseHash[:], si[62:94]) - var requester util.Uint160 - copy(requester[:], si[94:114]) - - return &state.VerificationRequest{ - ID: requestID, - RequestingChain: binary.BigEndian.Uint32(si[0:4]), - TargetChain: binary.BigEndian.Uint32(si[4:8]), - Subject: subject, - VerificationType: state.VerificationType(si[28]), - DataHash: dataHash, - Status: state.VerificationStatus(si[61]), - ResponseHash: responseHash, - Requester: requester, - CreatedAt: binary.BigEndian.Uint32(si[114:118]), - ExpiresAt: binary.BigEndian.Uint32(si[118:122]), - RespondedAt: binary.BigEndian.Uint32(si[122:126]), - }, true -} - -func (p *Pons) setVerificationInternal(d *dao.Simple, vr *state.VerificationRequest) { - buf := make([]byte, 126) - binary.BigEndian.PutUint32(buf[0:4], vr.RequestingChain) - binary.BigEndian.PutUint32(buf[4:8], vr.TargetChain) - copy(buf[8:28], vr.Subject[:]) - buf[28] = byte(vr.VerificationType) - copy(buf[29:61], vr.DataHash[:]) - buf[61] = byte(vr.Status) - copy(buf[62:94], vr.ResponseHash[:]) - copy(buf[94:114], vr.Requester[:]) - binary.BigEndian.PutUint32(buf[114:118], vr.CreatedAt) - binary.BigEndian.PutUint32(buf[118:122], vr.ExpiresAt) - binary.BigEndian.PutUint32(buf[122:126], vr.RespondedAt) - d.PutStorageItem(p.ID, makePonsVerificationKey(vr.ID), buf) - // Index by subject - d.PutStorageItem(p.ID, makePonsVerifBySubjectKey(vr.Subject, vr.ID), []byte{1}) -} - -// Settlement storage format: -// fromChain(4) + toChain(4) + sender(20) + receiver(20) + amount(8) + status(1) + -// createdAt(4) + settledAt(4) + txHash(32) + refLen(2) + reference(var) = 99 + ref bytes -func (p *Pons) getSettlementInternal(d *dao.Simple, settlementID uint64) (*state.SettlementRequest, bool) { - si := d.GetStorageItem(p.ID, makePonsSettlementKey(settlementID)) - if si == nil || len(si) < 99 { - return nil, false - } - - var sender util.Uint160 - copy(sender[:], si[8:28]) - var receiver util.Uint160 - copy(receiver[:], si[28:48]) - var txHash util.Uint256 - copy(txHash[:], si[65:97]) - - refLen := binary.BigEndian.Uint16(si[97:99]) - var reference string - if len(si) >= 99+int(refLen) { - reference = string(si[99 : 99+refLen]) - } - - return &state.SettlementRequest{ - ID: settlementID, - FromChain: binary.BigEndian.Uint32(si[0:4]), - ToChain: binary.BigEndian.Uint32(si[4:8]), - Sender: sender, - Receiver: receiver, - Amount: binary.BigEndian.Uint64(si[48:56]), - Status: state.SettlementStatus(si[56]), - CreatedAt: binary.BigEndian.Uint32(si[57:61]), - SettledAt: binary.BigEndian.Uint32(si[61:65]), - TxHash: txHash, - Reference: reference, - }, true -} - -func (p *Pons) setSettlementInternal(d *dao.Simple, sr *state.SettlementRequest) { - refBytes := []byte(sr.Reference) - buf := make([]byte, 99+len(refBytes)) - binary.BigEndian.PutUint32(buf[0:4], sr.FromChain) - binary.BigEndian.PutUint32(buf[4:8], sr.ToChain) - copy(buf[8:28], sr.Sender[:]) - copy(buf[28:48], sr.Receiver[:]) - binary.BigEndian.PutUint64(buf[48:56], sr.Amount) - buf[56] = byte(sr.Status) - binary.BigEndian.PutUint32(buf[57:61], sr.CreatedAt) - binary.BigEndian.PutUint32(buf[61:65], sr.SettledAt) - copy(buf[65:97], sr.TxHash[:]) - binary.BigEndian.PutUint16(buf[97:99], uint16(len(refBytes))) - copy(buf[99:], refBytes) - d.PutStorageItem(p.ID, makePonsSettlementKey(sr.ID), buf) - // Index by chain - d.PutStorageItem(p.ID, makePonsSettlByChainKey(sr.ToChain, sr.ID), []byte{1}) -} - -// CredentialShare storage format: -// sourceChain(4) + targetChain(4) + owner(20) + credentialType(1) + credentialID(8) + -// contentHash(32) + validUntil(4) + createdAt(4) + isRevoked(1) = 78 bytes -func (p *Pons) getCredentialShareInternal(d *dao.Simple, shareID uint64) (*state.CredentialShare, bool) { - si := d.GetStorageItem(p.ID, makePonsCredentialKey(shareID)) - if si == nil || len(si) < 78 { - return nil, false - } - - var owner util.Uint160 - copy(owner[:], si[8:28]) - var contentHash util.Uint256 - copy(contentHash[:], si[37:69]) - - return &state.CredentialShare{ - ID: shareID, - SourceChain: binary.BigEndian.Uint32(si[0:4]), - TargetChain: binary.BigEndian.Uint32(si[4:8]), - Owner: owner, - CredentialType: state.VerificationType(si[28]), - CredentialID: binary.BigEndian.Uint64(si[29:37]), - ContentHash: contentHash, - ValidUntil: binary.BigEndian.Uint32(si[69:73]), - CreatedAt: binary.BigEndian.Uint32(si[73:77]), - IsRevoked: si[77] != 0, - }, true -} - -func (p *Pons) setCredentialShareInternal(d *dao.Simple, cs *state.CredentialShare) { - buf := make([]byte, 78) - binary.BigEndian.PutUint32(buf[0:4], cs.SourceChain) - binary.BigEndian.PutUint32(buf[4:8], cs.TargetChain) - copy(buf[8:28], cs.Owner[:]) - buf[28] = byte(cs.CredentialType) - binary.BigEndian.PutUint64(buf[29:37], cs.CredentialID) - copy(buf[37:69], cs.ContentHash[:]) - binary.BigEndian.PutUint32(buf[69:73], cs.ValidUntil) - binary.BigEndian.PutUint32(buf[73:77], cs.CreatedAt) - if cs.IsRevoked { - buf[77] = 1 - } - d.PutStorageItem(p.ID, makePonsCredentialKey(cs.ID), buf) - // Index by owner - d.PutStorageItem(p.ID, makePonsCredByOwnerKey(cs.Owner, cs.ID), []byte{1}) -} - -// hasActiveAgreementInternal checks if there's an active agreement with the target chain -// for the specified agreement type. -func (p *Pons) hasActiveAgreementInternal(d *dao.Simple, remoteChainID uint32, agreementType state.AgreementType, blockHeight uint32) bool { - count := p.getCounterInternal(d, makePonsAgreementCounterKey()) - for i := uint64(1); i <= count; i++ { - agr, exists := p.getAgreementInternal(d, i) - if !exists { - continue - } - if agr.RemoteChainID != remoteChainID { - continue - } - if agr.Status != state.AgreementActive { - continue - } - // Check expiration - if agr.ExpirationDate > 0 && agr.ExpirationDate < blockHeight { - continue - } - // Check if agreement type matches or is comprehensive - if agr.AgreementType == agreementType || agr.AgreementType == state.AgreementTypeComprehensive { - return true - } - } - return false -} - -// ============================================================================ -// Contract Methods -// ============================================================================ - -func (p *Pons) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - cfg := p.getConfigInternal(ic.DAO) - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(cfg.LocalChainID))), - stackitem.NewBigInteger(big.NewInt(int64(cfg.VerificationTimeout))), - stackitem.NewBigInteger(big.NewInt(int64(cfg.SettlementTimeout))), - stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxPendingRequests)), - stackitem.NewBigInteger(big.NewInt(int64(cfg.CredentialShareExpiry))), - }) -} - -func (p *Pons) setLocalChainID(ic *interop.Context, args []stackitem.Item) stackitem.Item { - chainID := uint32(toBigInt(args[0]).Int64()) - - if !p.Tutus.CheckCommittee(ic) { - panic("only committee can set chain ID") - } - - cfg := p.getConfigInternal(ic.DAO) - cfg.LocalChainID = chainID - p.setConfigInternal(ic.DAO, cfg) - - return stackitem.NewBool(true) -} - -func (p *Pons) createAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - remoteChainID := uint32(toBigInt(args[0]).Int64()) - agreementType := state.AgreementType(toBigInt(args[1]).Int64()) - termsHashBytes, err := args[2].TryBytes() - if err != nil { - panic(err) - } - termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes) - if err != nil { - panic(err) - } - expirationHeight := uint32(toBigInt(args[3]).Int64()) - - if !p.Tutus.CheckCommittee(ic) { - panic("only committee can create agreements") - } - - if agreementType > state.AgreementTypeComprehensive { - panic(ErrInvalidAgreementType) - } - - cfg := p.getConfigInternal(ic.DAO) - - // Create agreement - agreementID := p.incrementCounterInternal(ic.DAO, makePonsAgreementCounterKey()) - agr := &state.BilateralAgreement{ - ID: agreementID, - LocalChainID: cfg.LocalChainID, - RemoteChainID: remoteChainID, - AgreementType: agreementType, - Status: state.AgreementPending, - Terms: termsHash, - EffectiveDate: 0, // Set when activated - ExpirationDate: expirationHeight, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - p.setAgreementInternal(ic.DAO, agr) - - ic.AddNotification(p.Hash, AgreementCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)), - stackitem.NewBigInteger(big.NewInt(int64(remoteChainID))), - stackitem.NewBigInteger(big.NewInt(int64(agreementType))), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)) -} - -func (p *Pons) updateAgreementStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { - agreementID := toBigInt(args[0]).Uint64() - newStatus := state.AgreementStatus(toBigInt(args[1]).Int64()) - - if !p.Tutus.CheckCommittee(ic) { - panic("only committee can update agreement status") - } - - agr, exists := p.getAgreementInternal(ic.DAO, agreementID) - if !exists { - panic(ErrAgreementNotFound) - } - - oldStatus := agr.Status - agr.Status = newStatus - agr.UpdatedAt = ic.Block.Index - - // Set effective date when activating - if newStatus == state.AgreementActive && oldStatus != state.AgreementActive { - agr.EffectiveDate = ic.Block.Index - } - - p.setAgreementInternal(ic.DAO, agr) - - ic.AddNotification(p.Hash, AgreementUpdatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)), - stackitem.NewBigInteger(big.NewInt(int64(oldStatus))), - stackitem.NewBigInteger(big.NewInt(int64(newStatus))), - })) - - return stackitem.NewBool(true) -} - -func (p *Pons) getAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - agreementID := toBigInt(args[0]).Uint64() - - agr, exists := p.getAgreementInternal(ic.DAO, agreementID) - if !exists { - return stackitem.Null{} - } - - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(agr.ID)), - stackitem.NewBigInteger(big.NewInt(int64(agr.LocalChainID))), - stackitem.NewBigInteger(big.NewInt(int64(agr.RemoteChainID))), - stackitem.NewBigInteger(big.NewInt(int64(agr.AgreementType))), - stackitem.NewBigInteger(big.NewInt(int64(agr.Status))), - stackitem.NewByteArray(agr.Terms.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(agr.EffectiveDate))), - stackitem.NewBigInteger(big.NewInt(int64(agr.ExpirationDate))), - stackitem.NewBigInteger(big.NewInt(int64(agr.CreatedAt))), - stackitem.NewBigInteger(big.NewInt(int64(agr.UpdatedAt))), - }) -} - -func (p *Pons) hasActiveAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - remoteChainID := uint32(toBigInt(args[0]).Int64()) - agreementType := state.AgreementType(toBigInt(args[1]).Int64()) - - has := p.hasActiveAgreementInternal(ic.DAO, remoteChainID, agreementType, ic.Block.Index) - return stackitem.NewBool(has) -} - -func (p *Pons) requestVerification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - targetChainID := uint32(toBigInt(args[0]).Int64()) - subject := toUint160(args[1]) - verificationType := state.VerificationType(toBigInt(args[2]).Int64()) - dataHashBytes, err := args[3].TryBytes() - if err != nil { - panic(err) - } - dataHash, err := util.Uint256DecodeBytesBE(dataHashBytes) - if err != nil { - panic(err) - } - - cfg := p.getConfigInternal(ic.DAO) - - // Check for active agreement - agreementType := state.AgreementTypeIdentity - switch verificationType { - case state.VerificationTypeCredential, state.VerificationTypeCertificate: - agreementType = state.AgreementTypeEducation - case state.VerificationTypeHealth: - agreementType = state.AgreementTypeHealthcare - } - - if !p.hasActiveAgreementInternal(ic.DAO, targetChainID, agreementType, ic.Block.Index) { - panic(ErrNoAgreement) - } - - // Get requester from caller - requester := ic.VM.GetCallingScriptHash() - - // Create verification request - requestID := p.incrementCounterInternal(ic.DAO, makePonsVerificationCounterKey()) - vr := &state.VerificationRequest{ - ID: requestID, - RequestingChain: cfg.LocalChainID, - TargetChain: targetChainID, - Subject: subject, - VerificationType: verificationType, - DataHash: dataHash, - Status: state.VerificationPending, - Requester: requester, - CreatedAt: ic.Block.Index, - ExpiresAt: ic.Block.Index + cfg.VerificationTimeout, - } - p.setVerificationInternal(ic.DAO, vr) - - ic.AddNotification(p.Hash, VerificationRequestedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)), - stackitem.NewBigInteger(big.NewInt(int64(targetChainID))), - stackitem.NewByteArray(subject.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(verificationType))), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)) -} - -func (p *Pons) respondVerification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - requestID := toBigInt(args[0]).Uint64() - approved := toBool(args[1]) - responseHashBytes, err := args[2].TryBytes() - if err != nil { - panic(err) - } - responseHash, err := util.Uint256DecodeBytesBE(responseHashBytes) - if err != nil { - panic(err) - } - - if !p.Tutus.CheckCommittee(ic) { - panic("only committee can respond to verification requests") - } - - vr, exists := p.getVerificationInternal(ic.DAO, requestID) - if !exists { - panic(ErrVerificationNotFound) - } - - if vr.Status != state.VerificationPending { - panic("verification request already processed") - } - - if ic.Block.Index > vr.ExpiresAt { - vr.Status = state.VerificationExpired - p.setVerificationInternal(ic.DAO, vr) - panic(ErrVerificationExpired) - } - - if approved { - vr.Status = state.VerificationApproved - } else { - vr.Status = state.VerificationRejected - } - vr.ResponseHash = responseHash - vr.RespondedAt = ic.Block.Index - p.setVerificationInternal(ic.DAO, vr) - - ic.AddNotification(p.Hash, VerificationRespondedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)), - stackitem.NewBool(approved), - })) - - return stackitem.NewBool(true) -} - -func (p *Pons) getVerificationRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item { - requestID := toBigInt(args[0]).Uint64() - - vr, exists := p.getVerificationInternal(ic.DAO, requestID) - if !exists { - return stackitem.Null{} - } - - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(vr.ID)), - stackitem.NewBigInteger(big.NewInt(int64(vr.RequestingChain))), - stackitem.NewBigInteger(big.NewInt(int64(vr.TargetChain))), - stackitem.NewByteArray(vr.Subject.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(vr.VerificationType))), - stackitem.NewByteArray(vr.DataHash.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(vr.Status))), - stackitem.NewByteArray(vr.ResponseHash.BytesBE()), - stackitem.NewByteArray(vr.Requester.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(vr.CreatedAt))), - stackitem.NewBigInteger(big.NewInt(int64(vr.ExpiresAt))), - stackitem.NewBigInteger(big.NewInt(int64(vr.RespondedAt))), - }) -} - -func (p *Pons) requestSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - toChainID := uint32(toBigInt(args[0]).Int64()) - receiver := toUint160(args[1]) - amount := toBigInt(args[2]).Uint64() - reference := toString(args[3]) - - cfg := p.getConfigInternal(ic.DAO) - - // Check for settlement agreement - if !p.hasActiveAgreementInternal(ic.DAO, toChainID, state.AgreementTypeSettlement, ic.Block.Index) { - panic(ErrNoAgreement) - } - - sender := ic.VM.GetCallingScriptHash() - - // Create settlement request - settlementID := p.incrementCounterInternal(ic.DAO, makePonsSettlementCounterKey()) - sr := &state.SettlementRequest{ - ID: settlementID, - FromChain: cfg.LocalChainID, - ToChain: toChainID, - Sender: sender, - Receiver: receiver, - Amount: amount, - Status: state.SettlementPending, - Reference: reference, - CreatedAt: ic.Block.Index, - } - p.setSettlementInternal(ic.DAO, sr) - - ic.AddNotification(p.Hash, SettlementRequestedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)), - stackitem.NewBigInteger(big.NewInt(int64(toChainID))), - stackitem.NewByteArray(receiver.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(amount)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)) -} - -func (p *Pons) completeSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - settlementID := toBigInt(args[0]).Uint64() - txHashBytes, err := args[1].TryBytes() - if err != nil { - panic(err) - } - txHash, err := util.Uint256DecodeBytesBE(txHashBytes) - if err != nil { - panic(err) - } - - if !p.Tutus.CheckCommittee(ic) { - panic("only committee can complete settlements") - } - - sr, exists := p.getSettlementInternal(ic.DAO, settlementID) - if !exists { - panic(ErrSettlementNotFound) - } - - if sr.Status != state.SettlementPending { - panic("settlement already processed") - } - - sr.Status = state.SettlementCompleted - sr.SettledAt = ic.Block.Index - sr.TxHash = txHash - p.setSettlementInternal(ic.DAO, sr) - - ic.AddNotification(p.Hash, SettlementCompletedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)), - stackitem.NewByteArray(txHash.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -func (p *Pons) cancelSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { - settlementID := toBigInt(args[0]).Uint64() - - sr, exists := p.getSettlementInternal(ic.DAO, settlementID) - if !exists { - panic(ErrSettlementNotFound) - } - - if sr.Status != state.SettlementPending { - panic("settlement already processed") - } - - // Allow sender or committee to cancel - caller := ic.VM.GetCallingScriptHash() - if !caller.Equals(sr.Sender) && !p.Tutus.CheckCommittee(ic) { - panic("only sender or committee can cancel settlement") - } - - sr.Status = state.SettlementCancelled - p.setSettlementInternal(ic.DAO, sr) - - return stackitem.NewBool(true) -} - -func (p *Pons) getSettlementRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item { - settlementID := toBigInt(args[0]).Uint64() - - sr, exists := p.getSettlementInternal(ic.DAO, settlementID) - if !exists { - return stackitem.Null{} - } - - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(sr.ID)), - stackitem.NewBigInteger(big.NewInt(int64(sr.FromChain))), - stackitem.NewBigInteger(big.NewInt(int64(sr.ToChain))), - stackitem.NewByteArray(sr.Sender.BytesBE()), - stackitem.NewByteArray(sr.Receiver.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(sr.Amount)), - stackitem.NewBigInteger(big.NewInt(int64(sr.Status))), - stackitem.NewByteArray(sr.TxHash.BytesBE()), - stackitem.NewByteArray([]byte(sr.Reference)), - stackitem.NewBigInteger(big.NewInt(int64(sr.CreatedAt))), - stackitem.NewBigInteger(big.NewInt(int64(sr.SettledAt))), - }) -} - -func (p *Pons) shareCredential(ic *interop.Context, args []stackitem.Item) stackitem.Item { - targetChainID := uint32(toBigInt(args[0]).Int64()) - credentialType := state.VerificationType(toBigInt(args[1]).Int64()) - credentialID := toBigInt(args[2]).Uint64() - contentHashBytes, err := args[3].TryBytes() - if err != nil { - panic(err) - } - contentHash, err := util.Uint256DecodeBytesBE(contentHashBytes) - if err != nil { - panic(err) - } - validUntil := uint32(toBigInt(args[4]).Int64()) - - cfg := p.getConfigInternal(ic.DAO) - - // Determine agreement type needed - agreementType := state.AgreementTypeEducation - if credentialType == state.VerificationTypeHealth { - agreementType = state.AgreementTypeHealthcare - } - - if !p.hasActiveAgreementInternal(ic.DAO, targetChainID, agreementType, ic.Block.Index) { - panic(ErrNoAgreement) - } - - owner := ic.VM.GetCallingScriptHash() - - // Set default validity if not provided - if validUntil == 0 { - validUntil = ic.Block.Index + cfg.CredentialShareExpiry - } - - // Create credential share - shareID := p.incrementCounterInternal(ic.DAO, makePonsCredentialCounterKey()) - cs := &state.CredentialShare{ - ID: shareID, - SourceChain: cfg.LocalChainID, - TargetChain: targetChainID, - Owner: owner, - CredentialType: credentialType, - CredentialID: credentialID, - ContentHash: contentHash, - ValidUntil: validUntil, - CreatedAt: ic.Block.Index, - IsRevoked: false, - } - p.setCredentialShareInternal(ic.DAO, cs) - - ic.AddNotification(p.Hash, CredentialSharedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)), - stackitem.NewByteArray(owner.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(targetChainID))), - stackitem.NewBigInteger(big.NewInt(int64(credentialType))), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)) -} - -func (p *Pons) revokeCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { - shareID := toBigInt(args[0]).Uint64() - - cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) - if !exists { - panic(ErrCredentialNotFound) - } - - // Allow owner or committee to revoke - caller := ic.VM.GetCallingScriptHash() - if !caller.Equals(cs.Owner) && !p.Tutus.CheckCommittee(ic) { - panic(ErrNotCredentialOwner) - } - - cs.IsRevoked = true - p.setCredentialShareInternal(ic.DAO, cs) - - ic.AddNotification(p.Hash, CredentialRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)), - stackitem.NewByteArray(cs.Owner.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -func (p *Pons) getCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { - shareID := toBigInt(args[0]).Uint64() - - cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) - if !exists { - return stackitem.Null{} - } - - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(cs.ID)), - stackitem.NewBigInteger(big.NewInt(int64(cs.SourceChain))), - stackitem.NewBigInteger(big.NewInt(int64(cs.TargetChain))), - stackitem.NewByteArray(cs.Owner.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(cs.CredentialType))), - stackitem.NewBigInteger(new(big.Int).SetUint64(cs.CredentialID)), - stackitem.NewByteArray(cs.ContentHash.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(cs.ValidUntil))), - stackitem.NewBigInteger(big.NewInt(int64(cs.CreatedAt))), - stackitem.NewBool(cs.IsRevoked), - }) -} - -func (p *Pons) verifyCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { - shareID := toBigInt(args[0]).Uint64() - - cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) - if !exists { - return stackitem.NewBool(false) - } - - if cs.IsRevoked { - return stackitem.NewBool(false) - } - - if ic.Block.Index > cs.ValidUntil { - return stackitem.NewBool(false) - } - - return stackitem.NewBool(true) -} - -func (p *Pons) getAgreementCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := p.getCounterInternal(ic.DAO, makePonsAgreementCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -func (p *Pons) getVerificationCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := p.getCounterInternal(ic.DAO, makePonsVerificationCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -func (p *Pons) getSettlementCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := p.getCounterInternal(ic.DAO, makePonsSettlementCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -func (p *Pons) getCredentialShareCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - count := p.getCounterInternal(ic.DAO, makePonsCredentialCounterKey()) - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} +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/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" +) + +// Pons ("bridge" in Latin) represents the Inter-Government Bridge Protocol +// native contract. It manages: +// - Bilateral agreements between sovereign chains +// - Cross-border verification requests +// - International VTS settlement +// - Education and healthcare credential portability +type Pons struct { + interop.ContractMD + Tutus ITutus + Vita IVita + Federation *Federation + RoleRegistry *RoleRegistry + VTS *VTS + Scire *Scire + Salus *Salus +} + +// Storage key prefixes for Pons. +const ( + ponsPrefixConfig byte = 0x01 // -> PonsConfig + ponsPrefixAgreement byte = 0x10 // agreementID -> BilateralAgreement + ponsPrefixAgreementByChain byte = 0x11 // chainID + agreementID -> exists + ponsPrefixAgreementCounter byte = 0x1F // -> next agreementID + ponsPrefixVerification byte = 0x20 // requestID -> VerificationRequest + ponsPrefixVerifBySubject byte = 0x21 // subject + requestID -> exists + ponsPrefixVerifCounter byte = 0x2F // -> next verificationID + ponsPrefixSettlement byte = 0x30 // settlementID -> SettlementRequest + ponsPrefixSettlByChain byte = 0x31 // chainID + settlementID -> exists + ponsPrefixSettlCounter byte = 0x3F // -> next settlementID + ponsPrefixCredential byte = 0x40 // credentialID -> CredentialShare + ponsPrefixCredByOwner byte = 0x41 // owner + credentialID -> exists + ponsPrefixCredCounter byte = 0x4F // -> next credentialID +) + +// Default configuration values. +const ( + defaultLocalChainID uint32 = 1 + defaultVerificationTimeout uint32 = 8640 // ~1 day at 10s blocks + defaultSettlementTimeout uint32 = 86400 // ~10 days + defaultMaxPendingRequests uint64 = 10000 + defaultCredentialShareExpiry uint32 = 315360 // ~1 year +) + +// Event names for Pons. +const ( + AgreementCreatedEvent = "AgreementCreated" + AgreementUpdatedEvent = "AgreementUpdated" + AgreementTerminatedEvent = "AgreementTerminated" + VerificationRequestedEvent = "VerificationRequested" + VerificationRespondedEvent = "VerificationResponded" + SettlementRequestedEvent = "SettlementRequested" + SettlementCompletedEvent = "SettlementCompleted" + CredentialSharedEvent = "CredentialShared" + CredentialRevokedEvent = "CredentialRevoked" +) + +// Various errors for Pons. +var ( + ErrNoAgreement = errors.New("no active agreement with target chain") + ErrAgreementExists = errors.New("agreement already exists") + ErrAgreementNotFound = errors.New("agreement not found") + ErrInvalidAgreementType = errors.New("invalid agreement type") + ErrAgreementNotActive = errors.New("agreement not active") + ErrVerificationNotFound = errors.New("verification request not found") + ErrVerificationExpired = errors.New("verification request expired") + ErrSettlementNotFound = errors.New("settlement request not found") + ErrSettlementExpired = errors.New("settlement request expired") + ErrCredentialNotFound = errors.New("credential share not found") + ErrCredentialExpired = errors.New("credential share expired") + ErrCredentialRevoked = errors.New("credential share revoked") + ErrMaxRequestsReached = errors.New("maximum pending requests reached") + ErrNotCredentialOwner = errors.New("not credential owner") + ErrAgreementTypeNotAllowed = errors.New("agreement type not allowed for this operation") +) + +var _ interop.Contract = (*Pons)(nil) + +// newPons creates a new Pons native contract. +func newPons() *Pons { + p := &Pons{ + ContractMD: *interop.NewContractMD(nativenames.Pons, nativeids.Pons), + } + defer p.BuildHFSpecificMD(p.ActiveIn()) + + // getConfig method + desc := NewDescriptor("getConfig", smartcontract.ArrayType) + md := NewMethodAndPrice(p.getConfig, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // setLocalChainID method (committee only) + desc = NewDescriptor("setLocalChainID", smartcontract.BoolType, + manifest.NewParameter("chainID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.setLocalChainID, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // --- Agreement Management --- + + // createAgreement method (committee only) + desc = NewDescriptor("createAgreement", smartcontract.IntegerType, + manifest.NewParameter("remoteChainID", smartcontract.IntegerType), + manifest.NewParameter("agreementType", smartcontract.IntegerType), + manifest.NewParameter("termsHash", smartcontract.Hash256Type), + manifest.NewParameter("expirationHeight", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.createAgreement, 1<<17, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // updateAgreementStatus method (committee only) + desc = NewDescriptor("updateAgreementStatus", smartcontract.BoolType, + manifest.NewParameter("agreementID", smartcontract.IntegerType), + manifest.NewParameter("newStatus", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.updateAgreementStatus, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // getAgreement method + desc = NewDescriptor("getAgreement", smartcontract.ArrayType, + manifest.NewParameter("agreementID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.getAgreement, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // hasActiveAgreement method + desc = NewDescriptor("hasActiveAgreement", smartcontract.BoolType, + manifest.NewParameter("remoteChainID", smartcontract.IntegerType), + manifest.NewParameter("agreementType", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.hasActiveAgreement, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // --- Verification Requests --- + + // requestVerification method + desc = NewDescriptor("requestVerification", smartcontract.IntegerType, + manifest.NewParameter("targetChainID", smartcontract.IntegerType), + manifest.NewParameter("subject", smartcontract.Hash160Type), + manifest.NewParameter("verificationType", smartcontract.IntegerType), + manifest.NewParameter("dataHash", smartcontract.Hash256Type)) + md = NewMethodAndPrice(p.requestVerification, 1<<17, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // respondVerification method (committee only - represents response from other chain) + desc = NewDescriptor("respondVerification", smartcontract.BoolType, + manifest.NewParameter("requestID", smartcontract.IntegerType), + manifest.NewParameter("approved", smartcontract.BoolType), + manifest.NewParameter("responseHash", smartcontract.Hash256Type)) + md = NewMethodAndPrice(p.respondVerification, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // getVerificationRequest method + desc = NewDescriptor("getVerificationRequest", smartcontract.ArrayType, + manifest.NewParameter("requestID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.getVerificationRequest, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // --- Settlement Requests --- + + // requestSettlement method + desc = NewDescriptor("requestSettlement", smartcontract.IntegerType, + manifest.NewParameter("toChainID", smartcontract.IntegerType), + manifest.NewParameter("receiver", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("reference", smartcontract.StringType)) + md = NewMethodAndPrice(p.requestSettlement, 1<<17, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // completeSettlement method (committee only - represents confirmation from other chain) + desc = NewDescriptor("completeSettlement", smartcontract.BoolType, + manifest.NewParameter("settlementID", smartcontract.IntegerType), + manifest.NewParameter("txHash", smartcontract.Hash256Type)) + md = NewMethodAndPrice(p.completeSettlement, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // cancelSettlement method (sender or committee) + desc = NewDescriptor("cancelSettlement", smartcontract.BoolType, + manifest.NewParameter("settlementID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.cancelSettlement, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // getSettlementRequest method + desc = NewDescriptor("getSettlementRequest", smartcontract.ArrayType, + manifest.NewParameter("settlementID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.getSettlementRequest, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // --- Credential Sharing --- + + // shareCredential method + desc = NewDescriptor("shareCredential", smartcontract.IntegerType, + manifest.NewParameter("targetChainID", smartcontract.IntegerType), + manifest.NewParameter("credentialType", smartcontract.IntegerType), + manifest.NewParameter("credentialID", smartcontract.IntegerType), + manifest.NewParameter("contentHash", smartcontract.Hash256Type), + manifest.NewParameter("validUntil", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.shareCredential, 1<<17, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // revokeCredentialShare method + desc = NewDescriptor("revokeCredentialShare", smartcontract.BoolType, + manifest.NewParameter("shareID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.revokeCredentialShare, 1<<16, callflag.States|callflag.AllowNotify) + p.AddMethod(md, desc) + + // getCredentialShare method + desc = NewDescriptor("getCredentialShare", smartcontract.ArrayType, + manifest.NewParameter("shareID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.getCredentialShare, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // verifyCredentialShare method + desc = NewDescriptor("verifyCredentialShare", smartcontract.BoolType, + manifest.NewParameter("shareID", smartcontract.IntegerType)) + md = NewMethodAndPrice(p.verifyCredentialShare, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // --- Counter Query Methods --- + + // getAgreementCount method + desc = NewDescriptor("getAgreementCount", smartcontract.IntegerType) + md = NewMethodAndPrice(p.getAgreementCount, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // getVerificationCount method + desc = NewDescriptor("getVerificationCount", smartcontract.IntegerType) + md = NewMethodAndPrice(p.getVerificationCount, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // getSettlementCount method + desc = NewDescriptor("getSettlementCount", smartcontract.IntegerType) + md = NewMethodAndPrice(p.getSettlementCount, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // getCredentialShareCount method + desc = NewDescriptor("getCredentialShareCount", smartcontract.IntegerType) + md = NewMethodAndPrice(p.getCredentialShareCount, 1<<15, callflag.ReadStates) + p.AddMethod(md, desc) + + // --- Events --- + + eDesc := NewEventDescriptor(AgreementCreatedEvent, + manifest.NewParameter("agreementID", smartcontract.IntegerType), + manifest.NewParameter("remoteChainID", smartcontract.IntegerType), + manifest.NewParameter("agreementType", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(AgreementUpdatedEvent, + manifest.NewParameter("agreementID", smartcontract.IntegerType), + manifest.NewParameter("oldStatus", smartcontract.IntegerType), + manifest.NewParameter("newStatus", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(AgreementTerminatedEvent, + manifest.NewParameter("agreementID", smartcontract.IntegerType), + manifest.NewParameter("remoteChainID", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(VerificationRequestedEvent, + manifest.NewParameter("requestID", smartcontract.IntegerType), + manifest.NewParameter("targetChainID", smartcontract.IntegerType), + manifest.NewParameter("subject", smartcontract.Hash160Type), + manifest.NewParameter("verificationType", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(VerificationRespondedEvent, + manifest.NewParameter("requestID", smartcontract.IntegerType), + manifest.NewParameter("approved", smartcontract.BoolType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(SettlementRequestedEvent, + manifest.NewParameter("settlementID", smartcontract.IntegerType), + manifest.NewParameter("toChainID", smartcontract.IntegerType), + manifest.NewParameter("receiver", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(SettlementCompletedEvent, + manifest.NewParameter("settlementID", smartcontract.IntegerType), + manifest.NewParameter("txHash", smartcontract.Hash256Type)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(CredentialSharedEvent, + manifest.NewParameter("shareID", smartcontract.IntegerType), + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("targetChainID", smartcontract.IntegerType), + manifest.NewParameter("credentialType", smartcontract.IntegerType)) + p.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(CredentialRevokedEvent, + manifest.NewParameter("shareID", smartcontract.IntegerType), + manifest.NewParameter("owner", smartcontract.Hash160Type)) + p.AddEvent(NewEvent(eDesc)) + + return p +} + +// Metadata returns contract metadata. +func (p *Pons) Metadata() *interop.ContractMD { + return &p.ContractMD +} + +// Initialize initializes Pons contract at the specified hardfork. +func (p *Pons) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != p.ActiveIn() { + return nil + } + + // Initialize default config + cfg := state.PonsConfig{ + LocalChainID: defaultLocalChainID, + VerificationTimeout: defaultVerificationTimeout, + SettlementTimeout: defaultSettlementTimeout, + MaxPendingRequests: defaultMaxPendingRequests, + CredentialShareExpiry: defaultCredentialShareExpiry, + } + p.setConfigInternal(ic.DAO, &cfg) + + return nil +} + +// InitializeCache fills native Pons cache from DAO on node restart. +func (p *Pons) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + return nil +} + +// OnPersist implements the Contract interface. +func (p *Pons) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist implements the Contract interface. +func (p *Pons) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn returns the hardfork this contract activates in (nil = always active). +func (p *Pons) ActiveIn() *config.Hardfork { + return nil +} + +// ============================================================================ +// Storage Key Helpers +// ============================================================================ + +func makePonsConfigKey() []byte { + return []byte{ponsPrefixConfig} +} + +func makePonsAgreementKey(agreementID uint64) []byte { + key := make([]byte, 9) + key[0] = ponsPrefixAgreement + binary.BigEndian.PutUint64(key[1:], agreementID) + return key +} + +func makePonsAgreementByChainKey(chainID uint32, agreementID uint64) []byte { + key := make([]byte, 13) + key[0] = ponsPrefixAgreementByChain + binary.BigEndian.PutUint32(key[1:], chainID) + binary.BigEndian.PutUint64(key[5:], agreementID) + return key +} + +func makePonsAgreementCounterKey() []byte { + return []byte{ponsPrefixAgreementCounter} +} + +func makePonsVerificationKey(requestID uint64) []byte { + key := make([]byte, 9) + key[0] = ponsPrefixVerification + binary.BigEndian.PutUint64(key[1:], requestID) + return key +} + +func makePonsVerifBySubjectKey(subject util.Uint160, requestID uint64) []byte { + key := make([]byte, 1+util.Uint160Size+8) + key[0] = ponsPrefixVerifBySubject + copy(key[1:], subject.BytesBE()) + binary.BigEndian.PutUint64(key[1+util.Uint160Size:], requestID) + return key +} + +func makePonsVerificationCounterKey() []byte { + return []byte{ponsPrefixVerifCounter} +} + +func makePonsSettlementKey(settlementID uint64) []byte { + key := make([]byte, 9) + key[0] = ponsPrefixSettlement + binary.BigEndian.PutUint64(key[1:], settlementID) + return key +} + +func makePonsSettlByChainKey(chainID uint32, settlementID uint64) []byte { + key := make([]byte, 13) + key[0] = ponsPrefixSettlByChain + binary.BigEndian.PutUint32(key[1:], chainID) + binary.BigEndian.PutUint64(key[5:], settlementID) + return key +} + +func makePonsSettlementCounterKey() []byte { + return []byte{ponsPrefixSettlCounter} +} + +func makePonsCredentialKey(credentialID uint64) []byte { + key := make([]byte, 9) + key[0] = ponsPrefixCredential + binary.BigEndian.PutUint64(key[1:], credentialID) + return key +} + +func makePonsCredByOwnerKey(owner util.Uint160, credentialID uint64) []byte { + key := make([]byte, 1+util.Uint160Size+8) + key[0] = ponsPrefixCredByOwner + copy(key[1:], owner.BytesBE()) + binary.BigEndian.PutUint64(key[1+util.Uint160Size:], credentialID) + return key +} + +func makePonsCredentialCounterKey() []byte { + return []byte{ponsPrefixCredCounter} +} + +// ============================================================================ +// Internal Storage Methods +// ============================================================================ + +func (p *Pons) getConfigInternal(d *dao.Simple) *state.PonsConfig { + si := d.GetStorageItem(p.ID, makePonsConfigKey()) + if si == nil { + return &state.PonsConfig{ + LocalChainID: defaultLocalChainID, + VerificationTimeout: defaultVerificationTimeout, + SettlementTimeout: defaultSettlementTimeout, + MaxPendingRequests: defaultMaxPendingRequests, + CredentialShareExpiry: defaultCredentialShareExpiry, + } + } + // Decode config: chainID(4) + verifTimeout(4) + settlTimeout(4) + maxReq(8) + credExpiry(4) = 24 bytes + if len(si) < 24 { + return &state.PonsConfig{ + LocalChainID: defaultLocalChainID, + VerificationTimeout: defaultVerificationTimeout, + SettlementTimeout: defaultSettlementTimeout, + MaxPendingRequests: defaultMaxPendingRequests, + CredentialShareExpiry: defaultCredentialShareExpiry, + } + } + return &state.PonsConfig{ + LocalChainID: binary.BigEndian.Uint32(si[0:4]), + VerificationTimeout: binary.BigEndian.Uint32(si[4:8]), + SettlementTimeout: binary.BigEndian.Uint32(si[8:12]), + MaxPendingRequests: binary.BigEndian.Uint64(si[12:20]), + CredentialShareExpiry: binary.BigEndian.Uint32(si[20:24]), + } +} + +func (p *Pons) setConfigInternal(d *dao.Simple, cfg *state.PonsConfig) { + buf := make([]byte, 24) + binary.BigEndian.PutUint32(buf[0:4], cfg.LocalChainID) + binary.BigEndian.PutUint32(buf[4:8], cfg.VerificationTimeout) + binary.BigEndian.PutUint32(buf[8:12], cfg.SettlementTimeout) + binary.BigEndian.PutUint64(buf[12:20], cfg.MaxPendingRequests) + binary.BigEndian.PutUint32(buf[20:24], cfg.CredentialShareExpiry) + d.PutStorageItem(p.ID, makePonsConfigKey(), buf) +} + +func (p *Pons) getCounterInternal(d *dao.Simple, key []byte) uint64 { + si := d.GetStorageItem(p.ID, key) + if si == nil || len(si) < 8 { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (p *Pons) incrementCounterInternal(d *dao.Simple, key []byte) uint64 { + current := p.getCounterInternal(d, key) + next := current + 1 + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, next) + d.PutStorageItem(p.ID, key, buf) + return next +} + +// Agreement storage format: +// localChainID(4) + remoteChainID(4) + agreementType(1) + status(1) + terms(32) + +// effectiveDate(4) + expirationDate(4) + createdAt(4) + updatedAt(4) = 58 bytes +func (p *Pons) getAgreementInternal(d *dao.Simple, agreementID uint64) (*state.BilateralAgreement, bool) { + si := d.GetStorageItem(p.ID, makePonsAgreementKey(agreementID)) + if si == nil || len(si) < 58 { + return nil, false + } + + var terms util.Uint256 + copy(terms[:], si[10:42]) + + return &state.BilateralAgreement{ + ID: agreementID, + LocalChainID: binary.BigEndian.Uint32(si[0:4]), + RemoteChainID: binary.BigEndian.Uint32(si[4:8]), + AgreementType: state.AgreementType(si[8]), + Status: state.AgreementStatus(si[9]), + Terms: terms, + EffectiveDate: binary.BigEndian.Uint32(si[42:46]), + ExpirationDate: binary.BigEndian.Uint32(si[46:50]), + CreatedAt: binary.BigEndian.Uint32(si[50:54]), + UpdatedAt: binary.BigEndian.Uint32(si[54:58]), + }, true +} + +func (p *Pons) setAgreementInternal(d *dao.Simple, agr *state.BilateralAgreement) { + buf := make([]byte, 58) + binary.BigEndian.PutUint32(buf[0:4], agr.LocalChainID) + binary.BigEndian.PutUint32(buf[4:8], agr.RemoteChainID) + buf[8] = byte(agr.AgreementType) + buf[9] = byte(agr.Status) + copy(buf[10:42], agr.Terms[:]) + binary.BigEndian.PutUint32(buf[42:46], agr.EffectiveDate) + binary.BigEndian.PutUint32(buf[46:50], agr.ExpirationDate) + binary.BigEndian.PutUint32(buf[50:54], agr.CreatedAt) + binary.BigEndian.PutUint32(buf[54:58], agr.UpdatedAt) + d.PutStorageItem(p.ID, makePonsAgreementKey(agr.ID), buf) + // Index by chain + d.PutStorageItem(p.ID, makePonsAgreementByChainKey(agr.RemoteChainID, agr.ID), []byte{1}) +} + +// Verification storage format: +// requestingChain(4) + targetChain(4) + subject(20) + verificationType(1) + dataHash(32) + +// status(1) + responseHash(32) + requester(20) + createdAt(4) + expiresAt(4) + respondedAt(4) = 126 bytes +func (p *Pons) getVerificationInternal(d *dao.Simple, requestID uint64) (*state.VerificationRequest, bool) { + si := d.GetStorageItem(p.ID, makePonsVerificationKey(requestID)) + if si == nil || len(si) < 126 { + return nil, false + } + + var subject util.Uint160 + copy(subject[:], si[8:28]) + var dataHash util.Uint256 + copy(dataHash[:], si[29:61]) + var responseHash util.Uint256 + copy(responseHash[:], si[62:94]) + var requester util.Uint160 + copy(requester[:], si[94:114]) + + return &state.VerificationRequest{ + ID: requestID, + RequestingChain: binary.BigEndian.Uint32(si[0:4]), + TargetChain: binary.BigEndian.Uint32(si[4:8]), + Subject: subject, + VerificationType: state.VerificationType(si[28]), + DataHash: dataHash, + Status: state.VerificationStatus(si[61]), + ResponseHash: responseHash, + Requester: requester, + CreatedAt: binary.BigEndian.Uint32(si[114:118]), + ExpiresAt: binary.BigEndian.Uint32(si[118:122]), + RespondedAt: binary.BigEndian.Uint32(si[122:126]), + }, true +} + +func (p *Pons) setVerificationInternal(d *dao.Simple, vr *state.VerificationRequest) { + buf := make([]byte, 126) + binary.BigEndian.PutUint32(buf[0:4], vr.RequestingChain) + binary.BigEndian.PutUint32(buf[4:8], vr.TargetChain) + copy(buf[8:28], vr.Subject[:]) + buf[28] = byte(vr.VerificationType) + copy(buf[29:61], vr.DataHash[:]) + buf[61] = byte(vr.Status) + copy(buf[62:94], vr.ResponseHash[:]) + copy(buf[94:114], vr.Requester[:]) + binary.BigEndian.PutUint32(buf[114:118], vr.CreatedAt) + binary.BigEndian.PutUint32(buf[118:122], vr.ExpiresAt) + binary.BigEndian.PutUint32(buf[122:126], vr.RespondedAt) + d.PutStorageItem(p.ID, makePonsVerificationKey(vr.ID), buf) + // Index by subject + d.PutStorageItem(p.ID, makePonsVerifBySubjectKey(vr.Subject, vr.ID), []byte{1}) +} + +// Settlement storage format: +// fromChain(4) + toChain(4) + sender(20) + receiver(20) + amount(8) + status(1) + +// createdAt(4) + settledAt(4) + txHash(32) + refLen(2) + reference(var) = 99 + ref bytes +func (p *Pons) getSettlementInternal(d *dao.Simple, settlementID uint64) (*state.SettlementRequest, bool) { + si := d.GetStorageItem(p.ID, makePonsSettlementKey(settlementID)) + if si == nil || len(si) < 99 { + return nil, false + } + + var sender util.Uint160 + copy(sender[:], si[8:28]) + var receiver util.Uint160 + copy(receiver[:], si[28:48]) + var txHash util.Uint256 + copy(txHash[:], si[65:97]) + + refLen := binary.BigEndian.Uint16(si[97:99]) + var reference string + if len(si) >= 99+int(refLen) { + reference = string(si[99 : 99+refLen]) + } + + return &state.SettlementRequest{ + ID: settlementID, + FromChain: binary.BigEndian.Uint32(si[0:4]), + ToChain: binary.BigEndian.Uint32(si[4:8]), + Sender: sender, + Receiver: receiver, + Amount: binary.BigEndian.Uint64(si[48:56]), + Status: state.SettlementStatus(si[56]), + CreatedAt: binary.BigEndian.Uint32(si[57:61]), + SettledAt: binary.BigEndian.Uint32(si[61:65]), + TxHash: txHash, + Reference: reference, + }, true +} + +func (p *Pons) setSettlementInternal(d *dao.Simple, sr *state.SettlementRequest) { + refBytes := []byte(sr.Reference) + buf := make([]byte, 99+len(refBytes)) + binary.BigEndian.PutUint32(buf[0:4], sr.FromChain) + binary.BigEndian.PutUint32(buf[4:8], sr.ToChain) + copy(buf[8:28], sr.Sender[:]) + copy(buf[28:48], sr.Receiver[:]) + binary.BigEndian.PutUint64(buf[48:56], sr.Amount) + buf[56] = byte(sr.Status) + binary.BigEndian.PutUint32(buf[57:61], sr.CreatedAt) + binary.BigEndian.PutUint32(buf[61:65], sr.SettledAt) + copy(buf[65:97], sr.TxHash[:]) + binary.BigEndian.PutUint16(buf[97:99], uint16(len(refBytes))) + copy(buf[99:], refBytes) + d.PutStorageItem(p.ID, makePonsSettlementKey(sr.ID), buf) + // Index by chain + d.PutStorageItem(p.ID, makePonsSettlByChainKey(sr.ToChain, sr.ID), []byte{1}) +} + +// CredentialShare storage format: +// sourceChain(4) + targetChain(4) + owner(20) + credentialType(1) + credentialID(8) + +// contentHash(32) + validUntil(4) + createdAt(4) + isRevoked(1) = 78 bytes +func (p *Pons) getCredentialShareInternal(d *dao.Simple, shareID uint64) (*state.CredentialShare, bool) { + si := d.GetStorageItem(p.ID, makePonsCredentialKey(shareID)) + if si == nil || len(si) < 78 { + return nil, false + } + + var owner util.Uint160 + copy(owner[:], si[8:28]) + var contentHash util.Uint256 + copy(contentHash[:], si[37:69]) + + return &state.CredentialShare{ + ID: shareID, + SourceChain: binary.BigEndian.Uint32(si[0:4]), + TargetChain: binary.BigEndian.Uint32(si[4:8]), + Owner: owner, + CredentialType: state.VerificationType(si[28]), + CredentialID: binary.BigEndian.Uint64(si[29:37]), + ContentHash: contentHash, + ValidUntil: binary.BigEndian.Uint32(si[69:73]), + CreatedAt: binary.BigEndian.Uint32(si[73:77]), + IsRevoked: si[77] != 0, + }, true +} + +func (p *Pons) setCredentialShareInternal(d *dao.Simple, cs *state.CredentialShare) { + buf := make([]byte, 78) + binary.BigEndian.PutUint32(buf[0:4], cs.SourceChain) + binary.BigEndian.PutUint32(buf[4:8], cs.TargetChain) + copy(buf[8:28], cs.Owner[:]) + buf[28] = byte(cs.CredentialType) + binary.BigEndian.PutUint64(buf[29:37], cs.CredentialID) + copy(buf[37:69], cs.ContentHash[:]) + binary.BigEndian.PutUint32(buf[69:73], cs.ValidUntil) + binary.BigEndian.PutUint32(buf[73:77], cs.CreatedAt) + if cs.IsRevoked { + buf[77] = 1 + } + d.PutStorageItem(p.ID, makePonsCredentialKey(cs.ID), buf) + // Index by owner + d.PutStorageItem(p.ID, makePonsCredByOwnerKey(cs.Owner, cs.ID), []byte{1}) +} + +// hasActiveAgreementInternal checks if there's an active agreement with the target chain +// for the specified agreement type. +func (p *Pons) hasActiveAgreementInternal(d *dao.Simple, remoteChainID uint32, agreementType state.AgreementType, blockHeight uint32) bool { + count := p.getCounterInternal(d, makePonsAgreementCounterKey()) + for i := uint64(1); i <= count; i++ { + agr, exists := p.getAgreementInternal(d, i) + if !exists { + continue + } + if agr.RemoteChainID != remoteChainID { + continue + } + if agr.Status != state.AgreementActive { + continue + } + // Check expiration + if agr.ExpirationDate > 0 && agr.ExpirationDate < blockHeight { + continue + } + // Check if agreement type matches or is comprehensive + if agr.AgreementType == agreementType || agr.AgreementType == state.AgreementTypeComprehensive { + return true + } + } + return false +} + +// ============================================================================ +// Contract Methods +// ============================================================================ + +func (p *Pons) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + cfg := p.getConfigInternal(ic.DAO) + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(cfg.LocalChainID))), + stackitem.NewBigInteger(big.NewInt(int64(cfg.VerificationTimeout))), + stackitem.NewBigInteger(big.NewInt(int64(cfg.SettlementTimeout))), + stackitem.NewBigInteger(new(big.Int).SetUint64(cfg.MaxPendingRequests)), + stackitem.NewBigInteger(big.NewInt(int64(cfg.CredentialShareExpiry))), + }) +} + +func (p *Pons) setLocalChainID(ic *interop.Context, args []stackitem.Item) stackitem.Item { + chainID := uint32(toBigInt(args[0]).Int64()) + + if !p.Tutus.CheckCommittee(ic) { + panic("only committee can set chain ID") + } + + cfg := p.getConfigInternal(ic.DAO) + cfg.LocalChainID = chainID + p.setConfigInternal(ic.DAO, cfg) + + return stackitem.NewBool(true) +} + +func (p *Pons) createAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + remoteChainID := uint32(toBigInt(args[0]).Int64()) + agreementType := state.AgreementType(toBigInt(args[1]).Int64()) + termsHashBytes, err := args[2].TryBytes() + if err != nil { + panic(err) + } + termsHash, err := util.Uint256DecodeBytesBE(termsHashBytes) + if err != nil { + panic(err) + } + expirationHeight := uint32(toBigInt(args[3]).Int64()) + + if !p.Tutus.CheckCommittee(ic) { + panic("only committee can create agreements") + } + + if agreementType > state.AgreementTypeComprehensive { + panic(ErrInvalidAgreementType) + } + + cfg := p.getConfigInternal(ic.DAO) + + // Create agreement + agreementID := p.incrementCounterInternal(ic.DAO, makePonsAgreementCounterKey()) + agr := &state.BilateralAgreement{ + ID: agreementID, + LocalChainID: cfg.LocalChainID, + RemoteChainID: remoteChainID, + AgreementType: agreementType, + Status: state.AgreementPending, + Terms: termsHash, + EffectiveDate: 0, // Set when activated + ExpirationDate: expirationHeight, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + p.setAgreementInternal(ic.DAO, agr) + + ic.AddNotification(p.Hash, AgreementCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)), + stackitem.NewBigInteger(big.NewInt(int64(remoteChainID))), + stackitem.NewBigInteger(big.NewInt(int64(agreementType))), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)) +} + +func (p *Pons) updateAgreementStatus(ic *interop.Context, args []stackitem.Item) stackitem.Item { + agreementID := toBigInt(args[0]).Uint64() + newStatus := state.AgreementStatus(toBigInt(args[1]).Int64()) + + if !p.Tutus.CheckCommittee(ic) { + panic("only committee can update agreement status") + } + + agr, exists := p.getAgreementInternal(ic.DAO, agreementID) + if !exists { + panic(ErrAgreementNotFound) + } + + oldStatus := agr.Status + agr.Status = newStatus + agr.UpdatedAt = ic.Block.Index + + // Set effective date when activating + if newStatus == state.AgreementActive && oldStatus != state.AgreementActive { + agr.EffectiveDate = ic.Block.Index + } + + p.setAgreementInternal(ic.DAO, agr) + + ic.AddNotification(p.Hash, AgreementUpdatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(agreementID)), + stackitem.NewBigInteger(big.NewInt(int64(oldStatus))), + stackitem.NewBigInteger(big.NewInt(int64(newStatus))), + })) + + return stackitem.NewBool(true) +} + +func (p *Pons) getAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + agreementID := toBigInt(args[0]).Uint64() + + agr, exists := p.getAgreementInternal(ic.DAO, agreementID) + if !exists { + return stackitem.Null{} + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(agr.ID)), + stackitem.NewBigInteger(big.NewInt(int64(agr.LocalChainID))), + stackitem.NewBigInteger(big.NewInt(int64(agr.RemoteChainID))), + stackitem.NewBigInteger(big.NewInt(int64(agr.AgreementType))), + stackitem.NewBigInteger(big.NewInt(int64(agr.Status))), + stackitem.NewByteArray(agr.Terms.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(agr.EffectiveDate))), + stackitem.NewBigInteger(big.NewInt(int64(agr.ExpirationDate))), + stackitem.NewBigInteger(big.NewInt(int64(agr.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(agr.UpdatedAt))), + }) +} + +func (p *Pons) hasActiveAgreement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + remoteChainID := uint32(toBigInt(args[0]).Int64()) + agreementType := state.AgreementType(toBigInt(args[1]).Int64()) + + has := p.hasActiveAgreementInternal(ic.DAO, remoteChainID, agreementType, ic.Block.Index) + return stackitem.NewBool(has) +} + +func (p *Pons) requestVerification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + targetChainID := uint32(toBigInt(args[0]).Int64()) + subject := toUint160(args[1]) + verificationType := state.VerificationType(toBigInt(args[2]).Int64()) + dataHashBytes, err := args[3].TryBytes() + if err != nil { + panic(err) + } + dataHash, err := util.Uint256DecodeBytesBE(dataHashBytes) + if err != nil { + panic(err) + } + + cfg := p.getConfigInternal(ic.DAO) + + // Check for active agreement + agreementType := state.AgreementTypeIdentity + switch verificationType { + case state.VerificationTypeCredential, state.VerificationTypeCertificate: + agreementType = state.AgreementTypeEducation + case state.VerificationTypeHealth: + agreementType = state.AgreementTypeHealthcare + } + + if !p.hasActiveAgreementInternal(ic.DAO, targetChainID, agreementType, ic.Block.Index) { + panic(ErrNoAgreement) + } + + // Get requester from caller + requester := ic.VM.GetCallingScriptHash() + + // Create verification request + requestID := p.incrementCounterInternal(ic.DAO, makePonsVerificationCounterKey()) + vr := &state.VerificationRequest{ + ID: requestID, + RequestingChain: cfg.LocalChainID, + TargetChain: targetChainID, + Subject: subject, + VerificationType: verificationType, + DataHash: dataHash, + Status: state.VerificationPending, + Requester: requester, + CreatedAt: ic.Block.Index, + ExpiresAt: ic.Block.Index + cfg.VerificationTimeout, + } + p.setVerificationInternal(ic.DAO, vr) + + ic.AddNotification(p.Hash, VerificationRequestedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)), + stackitem.NewBigInteger(big.NewInt(int64(targetChainID))), + stackitem.NewByteArray(subject.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(verificationType))), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)) +} + +func (p *Pons) respondVerification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + requestID := toBigInt(args[0]).Uint64() + approved := toBool(args[1]) + responseHashBytes, err := args[2].TryBytes() + if err != nil { + panic(err) + } + responseHash, err := util.Uint256DecodeBytesBE(responseHashBytes) + if err != nil { + panic(err) + } + + if !p.Tutus.CheckCommittee(ic) { + panic("only committee can respond to verification requests") + } + + vr, exists := p.getVerificationInternal(ic.DAO, requestID) + if !exists { + panic(ErrVerificationNotFound) + } + + if vr.Status != state.VerificationPending { + panic("verification request already processed") + } + + if ic.Block.Index > vr.ExpiresAt { + vr.Status = state.VerificationExpired + p.setVerificationInternal(ic.DAO, vr) + panic(ErrVerificationExpired) + } + + if approved { + vr.Status = state.VerificationApproved + } else { + vr.Status = state.VerificationRejected + } + vr.ResponseHash = responseHash + vr.RespondedAt = ic.Block.Index + p.setVerificationInternal(ic.DAO, vr) + + ic.AddNotification(p.Hash, VerificationRespondedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(requestID)), + stackitem.NewBool(approved), + })) + + return stackitem.NewBool(true) +} + +func (p *Pons) getVerificationRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item { + requestID := toBigInt(args[0]).Uint64() + + vr, exists := p.getVerificationInternal(ic.DAO, requestID) + if !exists { + return stackitem.Null{} + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(vr.ID)), + stackitem.NewBigInteger(big.NewInt(int64(vr.RequestingChain))), + stackitem.NewBigInteger(big.NewInt(int64(vr.TargetChain))), + stackitem.NewByteArray(vr.Subject.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(vr.VerificationType))), + stackitem.NewByteArray(vr.DataHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(vr.Status))), + stackitem.NewByteArray(vr.ResponseHash.BytesBE()), + stackitem.NewByteArray(vr.Requester.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(vr.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(vr.ExpiresAt))), + stackitem.NewBigInteger(big.NewInt(int64(vr.RespondedAt))), + }) +} + +func (p *Pons) requestSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + toChainID := uint32(toBigInt(args[0]).Int64()) + receiver := toUint160(args[1]) + amount := toBigInt(args[2]).Uint64() + reference := toString(args[3]) + + cfg := p.getConfigInternal(ic.DAO) + + // Check for settlement agreement + if !p.hasActiveAgreementInternal(ic.DAO, toChainID, state.AgreementTypeSettlement, ic.Block.Index) { + panic(ErrNoAgreement) + } + + sender := ic.VM.GetCallingScriptHash() + + // Create settlement request + settlementID := p.incrementCounterInternal(ic.DAO, makePonsSettlementCounterKey()) + sr := &state.SettlementRequest{ + ID: settlementID, + FromChain: cfg.LocalChainID, + ToChain: toChainID, + Sender: sender, + Receiver: receiver, + Amount: amount, + Status: state.SettlementPending, + Reference: reference, + CreatedAt: ic.Block.Index, + } + p.setSettlementInternal(ic.DAO, sr) + + ic.AddNotification(p.Hash, SettlementRequestedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)), + stackitem.NewBigInteger(big.NewInt(int64(toChainID))), + stackitem.NewByteArray(receiver.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(amount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)) +} + +func (p *Pons) completeSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + settlementID := toBigInt(args[0]).Uint64() + txHashBytes, err := args[1].TryBytes() + if err != nil { + panic(err) + } + txHash, err := util.Uint256DecodeBytesBE(txHashBytes) + if err != nil { + panic(err) + } + + if !p.Tutus.CheckCommittee(ic) { + panic("only committee can complete settlements") + } + + sr, exists := p.getSettlementInternal(ic.DAO, settlementID) + if !exists { + panic(ErrSettlementNotFound) + } + + if sr.Status != state.SettlementPending { + panic("settlement already processed") + } + + sr.Status = state.SettlementCompleted + sr.SettledAt = ic.Block.Index + sr.TxHash = txHash + p.setSettlementInternal(ic.DAO, sr) + + ic.AddNotification(p.Hash, SettlementCompletedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(settlementID)), + stackitem.NewByteArray(txHash.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +func (p *Pons) cancelSettlement(ic *interop.Context, args []stackitem.Item) stackitem.Item { + settlementID := toBigInt(args[0]).Uint64() + + sr, exists := p.getSettlementInternal(ic.DAO, settlementID) + if !exists { + panic(ErrSettlementNotFound) + } + + if sr.Status != state.SettlementPending { + panic("settlement already processed") + } + + // Allow sender or committee to cancel + caller := ic.VM.GetCallingScriptHash() + if !caller.Equals(sr.Sender) && !p.Tutus.CheckCommittee(ic) { + panic("only sender or committee can cancel settlement") + } + + sr.Status = state.SettlementCancelled + p.setSettlementInternal(ic.DAO, sr) + + return stackitem.NewBool(true) +} + +func (p *Pons) getSettlementRequest(ic *interop.Context, args []stackitem.Item) stackitem.Item { + settlementID := toBigInt(args[0]).Uint64() + + sr, exists := p.getSettlementInternal(ic.DAO, settlementID) + if !exists { + return stackitem.Null{} + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(sr.ID)), + stackitem.NewBigInteger(big.NewInt(int64(sr.FromChain))), + stackitem.NewBigInteger(big.NewInt(int64(sr.ToChain))), + stackitem.NewByteArray(sr.Sender.BytesBE()), + stackitem.NewByteArray(sr.Receiver.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(sr.Amount)), + stackitem.NewBigInteger(big.NewInt(int64(sr.Status))), + stackitem.NewByteArray(sr.TxHash.BytesBE()), + stackitem.NewByteArray([]byte(sr.Reference)), + stackitem.NewBigInteger(big.NewInt(int64(sr.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(sr.SettledAt))), + }) +} + +func (p *Pons) shareCredential(ic *interop.Context, args []stackitem.Item) stackitem.Item { + targetChainID := uint32(toBigInt(args[0]).Int64()) + credentialType := state.VerificationType(toBigInt(args[1]).Int64()) + credentialID := toBigInt(args[2]).Uint64() + contentHashBytes, err := args[3].TryBytes() + if err != nil { + panic(err) + } + contentHash, err := util.Uint256DecodeBytesBE(contentHashBytes) + if err != nil { + panic(err) + } + validUntil := uint32(toBigInt(args[4]).Int64()) + + cfg := p.getConfigInternal(ic.DAO) + + // Determine agreement type needed + agreementType := state.AgreementTypeEducation + if credentialType == state.VerificationTypeHealth { + agreementType = state.AgreementTypeHealthcare + } + + if !p.hasActiveAgreementInternal(ic.DAO, targetChainID, agreementType, ic.Block.Index) { + panic(ErrNoAgreement) + } + + owner := ic.VM.GetCallingScriptHash() + + // Set default validity if not provided + if validUntil == 0 { + validUntil = ic.Block.Index + cfg.CredentialShareExpiry + } + + // Create credential share + shareID := p.incrementCounterInternal(ic.DAO, makePonsCredentialCounterKey()) + cs := &state.CredentialShare{ + ID: shareID, + SourceChain: cfg.LocalChainID, + TargetChain: targetChainID, + Owner: owner, + CredentialType: credentialType, + CredentialID: credentialID, + ContentHash: contentHash, + ValidUntil: validUntil, + CreatedAt: ic.Block.Index, + IsRevoked: false, + } + p.setCredentialShareInternal(ic.DAO, cs) + + ic.AddNotification(p.Hash, CredentialSharedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)), + stackitem.NewByteArray(owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(targetChainID))), + stackitem.NewBigInteger(big.NewInt(int64(credentialType))), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)) +} + +func (p *Pons) revokeCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { + shareID := toBigInt(args[0]).Uint64() + + cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) + if !exists { + panic(ErrCredentialNotFound) + } + + // Allow owner or committee to revoke + caller := ic.VM.GetCallingScriptHash() + if !caller.Equals(cs.Owner) && !p.Tutus.CheckCommittee(ic) { + panic(ErrNotCredentialOwner) + } + + cs.IsRevoked = true + p.setCredentialShareInternal(ic.DAO, cs) + + ic.AddNotification(p.Hash, CredentialRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(shareID)), + stackitem.NewByteArray(cs.Owner.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +func (p *Pons) getCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { + shareID := toBigInt(args[0]).Uint64() + + cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) + if !exists { + return stackitem.Null{} + } + + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(cs.ID)), + stackitem.NewBigInteger(big.NewInt(int64(cs.SourceChain))), + stackitem.NewBigInteger(big.NewInt(int64(cs.TargetChain))), + stackitem.NewByteArray(cs.Owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(cs.CredentialType))), + stackitem.NewBigInteger(new(big.Int).SetUint64(cs.CredentialID)), + stackitem.NewByteArray(cs.ContentHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(cs.ValidUntil))), + stackitem.NewBigInteger(big.NewInt(int64(cs.CreatedAt))), + stackitem.NewBool(cs.IsRevoked), + }) +} + +func (p *Pons) verifyCredentialShare(ic *interop.Context, args []stackitem.Item) stackitem.Item { + shareID := toBigInt(args[0]).Uint64() + + cs, exists := p.getCredentialShareInternal(ic.DAO, shareID) + if !exists { + return stackitem.NewBool(false) + } + + if cs.IsRevoked { + return stackitem.NewBool(false) + } + + if ic.Block.Index > cs.ValidUntil { + return stackitem.NewBool(false) + } + + return stackitem.NewBool(true) +} + +func (p *Pons) getAgreementCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := p.getCounterInternal(ic.DAO, makePonsAgreementCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +func (p *Pons) getVerificationCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := p.getCounterInternal(ic.DAO, makePonsVerificationCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +func (p *Pons) getSettlementCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := p.getCounterInternal(ic.DAO, makePonsSettlementCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +func (p *Pons) getCredentialShareCount(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + count := p.getCounterInternal(ic.DAO, makePonsCredentialCounterKey()) + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} diff --git a/pkg/core/native/role_registry.go b/pkg/core/native/role_registry.go old mode 100644 new mode 100755 diff --git a/pkg/core/native/role_registry_domain.go b/pkg/core/native/role_registry_domain.go new file mode 100755 index 0000000..5174920 --- /dev/null +++ b/pkg/core/native/role_registry_domain.go @@ -0,0 +1,47 @@ +package native + +import ( + "github.com/tutus-one/tutus-chain/pkg/core/dao" + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// CRIT-002: Domain-specific committee roles for reduced single point of failure. +const ( + RoleCommitteeLegal uint64 = 100 + RoleCommitteeHealth uint64 = 101 + RoleCommitteeEducation uint64 = 102 + RoleCommitteeEconomy uint64 = 103 + RoleCommitteeIdentity uint64 = 104 + RoleCommitteeGovernance uint64 = 105 +) + +// CommitteeDomain represents a domain for committee authority. +type CommitteeDomain uint8 + +const ( + DomainLegal CommitteeDomain = iota + DomainHealth + DomainEducation + DomainEconomy + DomainIdentity + DomainGovernance +) + +// DomainCommitteeRole maps domains to their committee role IDs. +var DomainCommitteeRole = map[CommitteeDomain]uint64{ + DomainLegal: RoleCommitteeLegal, + DomainHealth: RoleCommitteeHealth, + DomainEducation: RoleCommitteeEducation, + DomainEconomy: RoleCommitteeEconomy, + DomainIdentity: RoleCommitteeIdentity, + DomainGovernance: RoleCommitteeGovernance, +} + +// HasDomainCommitteeAuthority checks if an address has committee authority for a specific domain. +func (r *RoleRegistry) HasDomainCommitteeAuthority(d *dao.Simple, address util.Uint160, domain CommitteeDomain, blockHeight uint32) bool { + roleID, ok := DomainCommitteeRole[domain] + if !ok { + return false + } + return r.HasRoleInternal(d, address, roleID, blockHeight) +} diff --git a/pkg/core/native/salus.go b/pkg/core/native/salus.go old mode 100644 new mode 100755 index bffb347..aa37876 --- a/pkg/core/native/salus.go +++ b/pkg/core/native/salus.go @@ -1,1516 +1,1516 @@ -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/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" -) - -// Salus represents the universal healthcare native contract. -type Salus struct { - interop.ContractMD - Tutus ITutus - Vita IVita - RoleRegistry IRoleRegistry - Lex ILex -} - -// SalusCache represents the cached state for Salus contract. -type SalusCache struct { - accountCount uint64 - recordCount uint64 - providerCount uint64 - authorizationCount uint64 - emergencyCount uint64 -} - -// Storage key prefixes for Salus. -const ( - salusPrefixAccount byte = 0x01 // vitaID -> HealthcareAccount - salusPrefixAccountByOwner byte = 0x02 // owner -> vitaID - salusPrefixRecord byte = 0x10 // recordID -> MedicalRecord - salusPrefixRecordByPatient byte = 0x11 // vitaID + recordID -> exists - salusPrefixRecordByProvider byte = 0x12 // provider + recordID -> exists - salusPrefixProvider byte = 0x20 // providerID -> HealthcareProvider - salusPrefixProviderByAddress byte = 0x21 // provider address -> providerID - salusPrefixAuthorization byte = 0x30 // authID -> ProviderAuthorization - salusPrefixAuthByPatient byte = 0x31 // vitaID + authID -> exists - salusPrefixAuthByProvider byte = 0x32 // provider + authID -> exists - salusPrefixActiveAuth byte = 0x33 // vitaID + provider -> authID - salusPrefixEmergencyAccess byte = 0x40 // emergencyID -> EmergencyAccess - salusPrefixEmergencyByPatient byte = 0x41 // vitaID + emergencyID -> exists - salusPrefixAccountCounter byte = 0xF0 // -> uint64 - salusPrefixRecordCounter byte = 0xF1 // -> next record ID - salusPrefixProviderCounter byte = 0xF2 // -> next provider ID - salusPrefixAuthCounter byte = 0xF3 // -> next authorization ID - salusPrefixEmergencyCounter byte = 0xF4 // -> next emergency access ID - salusPrefixConfig byte = 0xFF // -> SalusConfig -) - -// Event names for Salus. -const ( - HealthcareActivatedEvent = "HealthcareActivated" - CreditsAllocatedEventSalus = "CreditsAllocated" - MedicalRecordCreatedEvent = "MedicalRecordCreated" - ProviderRegisteredEvent = "ProviderRegistered" - ProviderSuspendedEvent = "ProviderSuspended" - AuthorizationGrantedEvent = "AuthorizationGranted" - AuthorizationRevokedEvent = "AuthorizationRevoked" - EmergencyAccessGrantedEvent = "EmergencyAccessGranted" - EmergencyAccessReviewedEvent = "EmergencyAccessReviewed" -) - -// Role constants for healthcare providers. -const ( - RoleHealthcare uint64 = 21 // Can record medical events and access authorized records -) - -// Various errors for Salus. -var ( - ErrSalusAccountNotFound = errors.New("healthcare account not found") - ErrSalusAccountExists = errors.New("healthcare account already exists") - ErrSalusAccountSuspended = errors.New("healthcare account is suspended") - ErrSalusAccountClosed = errors.New("healthcare account is closed") - ErrSalusNoVita = errors.New("owner must have an active Vita") - ErrSalusInsufficientCredits = errors.New("insufficient healthcare credits") - ErrSalusInvalidCredits = errors.New("invalid credit amount") - ErrSalusRecordNotFound = errors.New("medical record not found") - ErrSalusProviderNotFound = errors.New("healthcare provider not found") - ErrSalusProviderExists = errors.New("healthcare provider already registered") - ErrSalusProviderSuspended = errors.New("healthcare provider is suspended") - ErrSalusProviderRevoked = errors.New("healthcare provider is revoked") - ErrSalusNotProvider = errors.New("caller is not an authorized healthcare provider") - ErrSalusNotCommittee = errors.New("invalid committee signature") - ErrSalusInvalidOwner = errors.New("invalid owner address") - ErrSalusInvalidProvider = errors.New("invalid provider address") - ErrSalusAuthorizationNotFound = errors.New("authorization not found") - ErrSalusAuthorizationExpired = errors.New("authorization has expired") - ErrSalusAuthorizationExists = errors.New("authorization already exists") - ErrSalusNotPatient = errors.New("caller is not the patient") - ErrSalusHealthcareRestricted = errors.New("healthcare right is restricted") - ErrSalusEmergencyNotFound = errors.New("emergency access not found") - ErrSalusInvalidReason = errors.New("invalid reason") - ErrSalusInvalidName = errors.New("invalid name") - ErrSalusInvalidSpecialty = errors.New("invalid specialty") - ErrSalusNoAccess = errors.New("no access to patient records") - ErrSalusExceedsMaxDuration = errors.New("exceeds maximum authorization duration") -) - -var ( - _ interop.Contract = (*Salus)(nil) - _ dao.NativeContractCache = (*SalusCache)(nil) -) - -// Copy implements NativeContractCache interface. -func (c *SalusCache) Copy() dao.NativeContractCache { - return &SalusCache{ - accountCount: c.accountCount, - recordCount: c.recordCount, - providerCount: c.providerCount, - authorizationCount: c.authorizationCount, - emergencyCount: c.emergencyCount, - } -} - -// checkCommittee checks if the caller has committee authority. -func (s *Salus) checkCommittee(ic *interop.Context) bool { - if s.RoleRegistry != nil { - return s.RoleRegistry.CheckCommittee(ic) - } - return s.Tutus.CheckCommittee(ic) -} - -// checkHealthcareProvider checks if the caller has healthcare provider authority. -func (s *Salus) checkHealthcareProvider(ic *interop.Context) bool { - caller := ic.VM.GetCallingScriptHash() - if s.RoleRegistry != nil { - if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleHealthcare, ic.Block.Index) { - return true - } - } - // Committee members can also act as healthcare providers - return s.checkCommittee(ic) -} - -// checkHealthcareRight checks if subject has healthcare rights via Lex. -func (s *Salus) checkHealthcareRight(ic *interop.Context, subject util.Uint160) bool { - if s.Lex == nil { - return true // Allow if Lex not available - } - return s.Lex.HasRightInternal(ic.DAO, subject, state.RightHealthcare, ic.Block.Index) -} - -// newSalus creates a new Salus native contract. -func newSalus() *Salus { - s := &Salus{ - ContractMD: *interop.NewContractMD(nativenames.Salus, nativeids.Salus), - } - defer s.BuildHFSpecificMD(s.ActiveIn()) - - // ===== Account Management ===== - - // activateHealthcare - Activate healthcare account for a Vita holder - desc := NewDescriptor("activateHealthcare", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md := NewMethodAndPrice(s.activateHealthcare, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getAccount - Get healthcare account by owner - desc = NewDescriptor("getAccount", smartcontract.ArrayType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getAccountByVitaID - Get account by Vita ID - desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, - manifest.NewParameter("vitaID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // allocateCredits - Allocate healthcare credits (committee only) - desc = NewDescriptor("allocateCredits", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getCredits - Get available credits - desc = NewDescriptor("getCredits", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Medical Records ===== - - // recordMedicalEvent - Record a medical event (provider only) - desc = NewDescriptor("recordMedicalEvent", smartcontract.IntegerType, - manifest.NewParameter("patient", smartcontract.Hash160Type), - manifest.NewParameter("recordType", smartcontract.IntegerType), - manifest.NewParameter("contentHash", smartcontract.Hash256Type), - manifest.NewParameter("credits", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.recordMedicalEvent, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getMedicalRecord - Get medical record by ID - desc = NewDescriptor("getMedicalRecord", smartcontract.ArrayType, - manifest.NewParameter("recordID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getMedicalRecord, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Provider Management ===== - - // registerProvider - Register a healthcare provider (committee only) - desc = NewDescriptor("registerProvider", smartcontract.IntegerType, - manifest.NewParameter("address", smartcontract.Hash160Type), - manifest.NewParameter("name", smartcontract.StringType), - manifest.NewParameter("specialty", smartcontract.StringType), - manifest.NewParameter("licenseHash", smartcontract.Hash256Type)) - md = NewMethodAndPrice(s.registerProvider, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // suspendProvider - Suspend a healthcare provider (committee only) - desc = NewDescriptor("suspendProvider", smartcontract.BoolType, - manifest.NewParameter("providerID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.suspendProvider, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getProvider - Get provider details - desc = NewDescriptor("getProvider", smartcontract.ArrayType, - manifest.NewParameter("providerID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getProvider, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getProviderByAddress - Get provider by address - desc = NewDescriptor("getProviderByAddress", smartcontract.ArrayType, - manifest.NewParameter("address", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getProviderByAddress, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Authorization Management ===== - - // authorizeAccess - Grant provider access to patient records - desc = NewDescriptor("authorizeAccess", smartcontract.IntegerType, - manifest.NewParameter("patient", smartcontract.Hash160Type), - manifest.NewParameter("provider", smartcontract.Hash160Type), - manifest.NewParameter("accessLevel", smartcontract.IntegerType), - manifest.NewParameter("duration", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.authorizeAccess, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // revokeAccess - Revoke provider access - desc = NewDescriptor("revokeAccess", smartcontract.BoolType, - manifest.NewParameter("authID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.revokeAccess, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getAuthorization - Get authorization details - desc = NewDescriptor("getAuthorization", smartcontract.ArrayType, - manifest.NewParameter("authID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getAuthorization, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // hasAccess - Check if provider has access to patient - desc = NewDescriptor("hasAccess", smartcontract.BoolType, - manifest.NewParameter("patient", smartcontract.Hash160Type), - manifest.NewParameter("provider", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.hasAccess, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Emergency Access ===== - - // emergencyAccess - Request emergency access (provider only) - desc = NewDescriptor("emergencyAccess", smartcontract.IntegerType, - manifest.NewParameter("patient", smartcontract.Hash160Type), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.emergencyAccess, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // reviewEmergencyAccess - Review emergency access (committee only) - desc = NewDescriptor("reviewEmergencyAccess", smartcontract.BoolType, - manifest.NewParameter("emergencyID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.reviewEmergencyAccess, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getEmergencyAccess - Get emergency access details - desc = NewDescriptor("getEmergencyAccess", smartcontract.ArrayType, - manifest.NewParameter("emergencyID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getEmergencyAccess, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Query Methods ===== - - // getConfig - Get Salus configuration - desc = NewDescriptor("getConfig", smartcontract.ArrayType) - md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalAccounts - Get total healthcare accounts - desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalRecords - Get total medical records - desc = NewDescriptor("getTotalRecords", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalRecords, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalProviders - Get total healthcare providers - desc = NewDescriptor("getTotalProviders", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalProviders, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Events ===== - - // HealthcareActivated event - eDesc := NewEventDescriptor(HealthcareActivatedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("owner", smartcontract.Hash160Type)) - s.AddEvent(NewEvent(eDesc)) - - // CreditsAllocated event - eDesc = NewEventDescriptor(CreditsAllocatedEventSalus, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("total", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - // MedicalRecordCreated event - eDesc = NewEventDescriptor(MedicalRecordCreatedEvent, - manifest.NewParameter("recordID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("recordType", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - // ProviderRegistered event - eDesc = NewEventDescriptor(ProviderRegisteredEvent, - manifest.NewParameter("providerID", smartcontract.IntegerType), - manifest.NewParameter("address", smartcontract.Hash160Type), - manifest.NewParameter("specialty", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // ProviderSuspended event - eDesc = NewEventDescriptor(ProviderSuspendedEvent, - manifest.NewParameter("providerID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // AuthorizationGranted event - eDesc = NewEventDescriptor(AuthorizationGrantedEvent, - manifest.NewParameter("authID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("provider", smartcontract.Hash160Type)) - s.AddEvent(NewEvent(eDesc)) - - // AuthorizationRevoked event - eDesc = NewEventDescriptor(AuthorizationRevokedEvent, - manifest.NewParameter("authID", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - // EmergencyAccessGranted event - eDesc = NewEventDescriptor(EmergencyAccessGrantedEvent, - manifest.NewParameter("emergencyID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("provider", smartcontract.Hash160Type)) - s.AddEvent(NewEvent(eDesc)) - - // EmergencyAccessReviewed event - eDesc = NewEventDescriptor(EmergencyAccessReviewedEvent, - manifest.NewParameter("emergencyID", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - return s -} - -// Metadata returns contract metadata. -func (s *Salus) Metadata() *interop.ContractMD { - return &s.ContractMD -} - -// Initialize initializes the Salus contract. -func (s *Salus) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != s.ActiveIn() { - return nil - } - - // Initialize counters - s.setAccountCounter(ic.DAO, 0) - s.setRecordCounter(ic.DAO, 0) - s.setProviderCounter(ic.DAO, 0) - s.setAuthCounter(ic.DAO, 0) - s.setEmergencyCounter(ic.DAO, 0) - - // Initialize config with defaults - cfg := &state.SalusConfig{ - DefaultAnnualCredits: 10000, // 10000 healthcare credits per year - EmergencyAccessDuration: 86400, // ~24 hours (1-second blocks) - PreventiveCareBonus: 500, // Bonus for preventive care visits - MaxAuthorizationDuration: 2592000, // ~30 days (1-second blocks) - } - s.setConfig(ic.DAO, cfg) - - // Initialize cache - cache := &SalusCache{ - accountCount: 0, - recordCount: 0, - providerCount: 0, - authorizationCount: 0, - emergencyCount: 0, - } - ic.DAO.SetCache(s.ID, cache) - - return nil -} - -// InitializeCache initializes the cache from storage. -func (s *Salus) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - cache := &SalusCache{ - accountCount: s.getAccountCounter(d), - recordCount: s.getRecordCounter(d), - providerCount: s.getProviderCounter(d), - authorizationCount: s.getAuthCounter(d), - emergencyCount: s.getEmergencyCounter(d), - } - d.SetCache(s.ID, cache) - return nil -} - -// OnPersist is called before block is committed. -func (s *Salus) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist is called after block is committed. -func (s *Salus) PostPersist(ic *interop.Context) error { - return nil -} - -// ActiveIn returns the hardfork at which this contract is activated. -func (s *Salus) ActiveIn() *config.Hardfork { - return nil // Always active -} - -// ===== Storage Helpers ===== - -func (s *Salus) makeAccountKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = salusPrefixAccount - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -func (s *Salus) makeAccountByOwnerKey(owner util.Uint160) []byte { - key := make([]byte, 21) - key[0] = salusPrefixAccountByOwner - copy(key[1:], owner.BytesBE()) - return key -} - -func (s *Salus) makeRecordKey(recordID uint64) []byte { - key := make([]byte, 9) - key[0] = salusPrefixRecord - binary.BigEndian.PutUint64(key[1:], recordID) - return key -} - -func (s *Salus) makeRecordByPatientKey(vitaID, recordID uint64) []byte { - key := make([]byte, 17) - key[0] = salusPrefixRecordByPatient - binary.BigEndian.PutUint64(key[1:9], vitaID) - binary.BigEndian.PutUint64(key[9:], recordID) - return key -} - -func (s *Salus) makeProviderKey(providerID uint64) []byte { - key := make([]byte, 9) - key[0] = salusPrefixProvider - binary.BigEndian.PutUint64(key[1:], providerID) - return key -} - -func (s *Salus) makeProviderByAddressKey(address util.Uint160) []byte { - key := make([]byte, 21) - key[0] = salusPrefixProviderByAddress - copy(key[1:], address.BytesBE()) - return key -} - -func (s *Salus) makeAuthorizationKey(authID uint64) []byte { - key := make([]byte, 9) - key[0] = salusPrefixAuthorization - binary.BigEndian.PutUint64(key[1:], authID) - return key -} - -func (s *Salus) makeActiveAuthKey(vitaID uint64, provider util.Uint160) []byte { - key := make([]byte, 29) - key[0] = salusPrefixActiveAuth - binary.BigEndian.PutUint64(key[1:9], vitaID) - copy(key[9:], provider.BytesBE()) - return key -} - -func (s *Salus) makeEmergencyKey(emergencyID uint64) []byte { - key := make([]byte, 9) - key[0] = salusPrefixEmergencyAccess - binary.BigEndian.PutUint64(key[1:], emergencyID) - return key -} - -// Counter getters/setters -func (s *Salus) getAccountCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{salusPrefixAccountCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Salus) setAccountCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{salusPrefixAccountCounter}, buf) -} - -func (s *Salus) getRecordCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{salusPrefixRecordCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Salus) setRecordCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{salusPrefixRecordCounter}, buf) -} - -func (s *Salus) getProviderCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{salusPrefixProviderCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Salus) setProviderCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{salusPrefixProviderCounter}, buf) -} - -func (s *Salus) getAuthCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{salusPrefixAuthCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Salus) setAuthCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{salusPrefixAuthCounter}, buf) -} - -func (s *Salus) getEmergencyCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{salusPrefixEmergencyCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Salus) setEmergencyCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{salusPrefixEmergencyCounter}, buf) -} - -// Config getter/setter -func (s *Salus) getConfigInternal(d *dao.Simple) *state.SalusConfig { - si := d.GetStorageItem(s.ID, []byte{salusPrefixConfig}) - if si == nil { - return &state.SalusConfig{ - DefaultAnnualCredits: 10000, - EmergencyAccessDuration: 86400, - PreventiveCareBonus: 500, - MaxAuthorizationDuration: 2592000, - } - } - cfg := new(state.SalusConfig) - item, _ := stackitem.Deserialize(si) - cfg.FromStackItem(item) - return cfg -} - -func (s *Salus) setConfig(d *dao.Simple, cfg *state.SalusConfig) { - item, _ := cfg.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, []byte{salusPrefixConfig}, data) -} - -// Account storage -func (s *Salus) getAccountInternal(d *dao.Simple, vitaID uint64) *state.HealthcareAccount { - si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID)) - if si == nil { - return nil - } - acc := new(state.HealthcareAccount) - item, _ := stackitem.Deserialize(si) - acc.FromStackItem(item) - return acc -} - -func (s *Salus) putAccount(d *dao.Simple, acc *state.HealthcareAccount) { - item, _ := acc.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data) -} - -func (s *Salus) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { - si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (s *Salus) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, vitaID) - d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf) -} - -// Record storage -func (s *Salus) getRecordInternal(d *dao.Simple, recordID uint64) *state.MedicalRecord { - si := d.GetStorageItem(s.ID, s.makeRecordKey(recordID)) - if si == nil { - return nil - } - record := new(state.MedicalRecord) - item, _ := stackitem.Deserialize(si) - record.FromStackItem(item) - return record -} - -func (s *Salus) putRecord(d *dao.Simple, record *state.MedicalRecord) { - item, _ := record.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeRecordKey(record.ID), data) -} - -func (s *Salus) setRecordByPatient(d *dao.Simple, vitaID, recordID uint64) { - d.PutStorageItem(s.ID, s.makeRecordByPatientKey(vitaID, recordID), []byte{1}) -} - -// Provider storage -func (s *Salus) getProviderInternal(d *dao.Simple, providerID uint64) *state.HealthcareProvider { - si := d.GetStorageItem(s.ID, s.makeProviderKey(providerID)) - if si == nil { - return nil - } - provider := new(state.HealthcareProvider) - item, _ := stackitem.Deserialize(si) - provider.FromStackItem(item) - return provider -} - -func (s *Salus) putProvider(d *dao.Simple, provider *state.HealthcareProvider) { - item, _ := provider.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeProviderKey(provider.ProviderID), data) -} - -func (s *Salus) getProviderIDByAddress(d *dao.Simple, address util.Uint160) (uint64, bool) { - si := d.GetStorageItem(s.ID, s.makeProviderByAddressKey(address)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (s *Salus) setProviderByAddress(d *dao.Simple, address util.Uint160, providerID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, providerID) - d.PutStorageItem(s.ID, s.makeProviderByAddressKey(address), buf) -} - -// Authorization storage -func (s *Salus) getAuthInternal(d *dao.Simple, authID uint64) *state.ProviderAuthorization { - si := d.GetStorageItem(s.ID, s.makeAuthorizationKey(authID)) - if si == nil { - return nil - } - auth := new(state.ProviderAuthorization) - item, _ := stackitem.Deserialize(si) - auth.FromStackItem(item) - return auth -} - -func (s *Salus) putAuth(d *dao.Simple, auth *state.ProviderAuthorization) { - item, _ := auth.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeAuthorizationKey(auth.ID), data) -} - -func (s *Salus) getActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160) (uint64, bool) { - si := d.GetStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (s *Salus) setActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160, authID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, authID) - d.PutStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider), buf) -} - -func (s *Salus) clearActiveAuth(d *dao.Simple, vitaID uint64, provider util.Uint160) { - d.DeleteStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider)) -} - -// Emergency access storage -func (s *Salus) getEmergencyInternal(d *dao.Simple, emergencyID uint64) *state.EmergencyAccess { - si := d.GetStorageItem(s.ID, s.makeEmergencyKey(emergencyID)) - if si == nil { - return nil - } - emergency := new(state.EmergencyAccess) - item, _ := stackitem.Deserialize(si) - emergency.FromStackItem(item) - return emergency -} - -func (s *Salus) putEmergency(d *dao.Simple, emergency *state.EmergencyAccess) { - item, _ := emergency.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeEmergencyKey(emergency.ID), data) -} - -// ===== Contract Methods ===== - -// activateHealthcare activates healthcare account for a Vita holder. -func (s *Salus) activateHealthcare(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - // Check owner has active Vita - if s.Vita == nil { - panic(ErrSalusNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil { - panic(ErrSalusNoVita) - } - if vita.Status != state.TokenStatusActive { - panic(ErrSalusNoVita) - } - - // Check if account already exists - existing := s.getAccountInternal(ic.DAO, vita.TokenID) - if existing != nil { - panic(ErrSalusAccountExists) - } - - // Check healthcare rights - if !s.checkHealthcareRight(ic, owner) { - // Log but allow (EnforcementLogging) - } - - // Get cache and increment counter - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - cache.accountCount++ - s.setAccountCounter(ic.DAO, cache.accountCount) - - // Get default credits from config - cfg := s.getConfigInternal(ic.DAO) - - // Create account - acc := &state.HealthcareAccount{ - VitaID: vita.TokenID, - Owner: owner, - AnnualAllocation: cfg.DefaultAnnualCredits, - CreditsUsed: 0, - CreditsAvailable: cfg.DefaultAnnualCredits, - BiologicalAge: 0, - LastCheckup: 0, - Status: state.HealthcareAccountActive, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - - // Store account - s.putAccount(ic.DAO, acc) - s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID) - - // Emit event - ic.AddNotification(s.Hash, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), - stackitem.NewByteArray(owner.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -// getAccount returns healthcare account by owner. -func (s *Salus) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.Null{} - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.Null{} - } - - item, _ := acc.ToStackItem() - return item -} - -// getAccountByVitaID returns healthcare account by Vita ID. -func (s *Salus) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { - vitaID := toUint64(args[0]) - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.Null{} - } - - item, _ := acc.ToStackItem() - return item -} - -// allocateCredits allocates healthcare credits to an account (committee only). -func (s *Salus) allocateCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - amount := toUint64(args[1]) - // reason := toString(args[2]) // for logging - - // Committee only - if !s.checkCommittee(ic) { - panic(ErrSalusNotCommittee) - } - - if amount == 0 { - panic(ErrSalusInvalidCredits) - } - - // Get or create account - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - // Auto-create account if Vita exists - if s.Vita == nil { - panic(ErrSalusNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil || vita.Status != state.TokenStatusActive { - panic(ErrSalusNoVita) - } - vitaID = vita.TokenID - - cfg := s.getConfigInternal(ic.DAO) - - // Create account - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - cache.accountCount++ - s.setAccountCounter(ic.DAO, cache.accountCount) - - acc := &state.HealthcareAccount{ - VitaID: vitaID, - Owner: owner, - AnnualAllocation: cfg.DefaultAnnualCredits, - CreditsUsed: 0, - CreditsAvailable: amount, - BiologicalAge: 0, - LastCheckup: 0, - Status: state.HealthcareAccountActive, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - s.putAccount(ic.DAO, acc) - s.setOwnerToVitaID(ic.DAO, owner, vitaID) - - // Emit events - ic.AddNotification(s.Hash, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewByteArray(owner.BytesBE()), - })) - ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - })) - - return stackitem.NewBool(true) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - panic(ErrSalusAccountNotFound) - } - if acc.Status != state.HealthcareAccountActive { - panic(ErrSalusAccountSuspended) - } - - // Add credits - acc.CreditsAvailable += amount - acc.UpdatedAt = ic.Block.Index - - s.putAccount(ic.DAO, acc) - - // Emit event - ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - stackitem.NewBigInteger(big.NewInt(int64(acc.CreditsAvailable))), - })) - - return stackitem.NewBool(true) -} - -// getCredits returns available credits for an owner. -func (s *Salus) getCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - return stackitem.NewBigInteger(big.NewInt(int64(acc.CreditsAvailable))) -} - -// recordMedicalEvent records a medical event (provider only). -func (s *Salus) recordMedicalEvent(ic *interop.Context, args []stackitem.Item) stackitem.Item { - patient := toUint160(args[0]) - recordType := state.MedicalRecordType(toUint64(args[1])) - contentHashBytes := toBytes(args[2]) - credits := toUint64(args[3]) - - // Convert bytes to Uint256 - var contentHash util.Uint256 - if len(contentHashBytes) == 32 { - copy(contentHash[:], contentHashBytes) - } - - // Check provider authority - if !s.checkHealthcareProvider(ic) { - panic(ErrSalusNotProvider) - } - - // Get provider address - provider := ic.VM.GetCallingScriptHash() - - // Get patient's Vita - if s.Vita == nil { - panic(ErrSalusNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient) - if err != nil || vita == nil || vita.Status != state.TokenStatusActive { - panic(ErrSalusNoVita) - } - - // Check provider has access (unless emergency or authorized) - vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) - if found { - authID, hasAuth := s.getActiveAuthID(ic.DAO, vitaID, provider) - if hasAuth { - auth := s.getAuthInternal(ic.DAO, authID) - if auth == nil || !auth.IsValid(ic.Block.Index) { - panic(ErrSalusNoAccess) - } - } - // If no auth, still allow (emergency can be logged separately) - } - - // Get or auto-create account - if !found { - // Auto-create - cfg := s.getConfigInternal(ic.DAO) - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - cache.accountCount++ - s.setAccountCounter(ic.DAO, cache.accountCount) - - acc := &state.HealthcareAccount{ - VitaID: vita.TokenID, - Owner: patient, - AnnualAllocation: cfg.DefaultAnnualCredits, - CreditsUsed: 0, - CreditsAvailable: cfg.DefaultAnnualCredits, - BiologicalAge: 0, - LastCheckup: 0, - Status: state.HealthcareAccountActive, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - s.putAccount(ic.DAO, acc) - s.setOwnerToVitaID(ic.DAO, patient, vita.TokenID) - vitaID = vita.TokenID - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - panic(ErrSalusAccountNotFound) - } - if acc.Status != state.HealthcareAccountActive { - panic(ErrSalusAccountSuspended) - } - - // Check sufficient credits - if acc.CreditsAvailable < credits { - panic(ErrSalusInsufficientCredits) - } - - // Get next record ID - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - recordID := cache.recordCount - cache.recordCount++ - s.setRecordCounter(ic.DAO, cache.recordCount) - - // Deduct credits - acc.CreditsUsed += credits - acc.CreditsAvailable -= credits - acc.UpdatedAt = ic.Block.Index - - // Update last checkup if appropriate - if recordType == state.RecordTypeCheckup || recordType == state.RecordTypePreventive { - acc.LastCheckup = ic.Block.Index - } - - s.putAccount(ic.DAO, acc) - - // Create record - record := &state.MedicalRecord{ - ID: recordID, - VitaID: vitaID, - Patient: patient, - Provider: provider, - RecordType: recordType, - ContentHash: contentHash, - CreditsUsed: credits, - CreatedAt: ic.Block.Index, - IsActive: true, - } - - s.putRecord(ic.DAO, record) - s.setRecordByPatient(ic.DAO, vitaID, recordID) - - // Emit event - ic.AddNotification(s.Hash, MedicalRecordCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(recordID))), - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewBigInteger(big.NewInt(int64(recordType))), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(recordID))) -} - -// getMedicalRecord returns medical record by ID. -func (s *Salus) getMedicalRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item { - recordID := toUint64(args[0]) - - record := s.getRecordInternal(ic.DAO, recordID) - if record == nil { - return stackitem.Null{} - } - - item, _ := record.ToStackItem() - return item -} - -// registerProvider registers a healthcare provider (committee only). -func (s *Salus) registerProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { - address := toUint160(args[0]) - name := toString(args[1]) - specialty := toString(args[2]) - licenseHashBytes := toBytes(args[3]) - - // Convert bytes to Uint256 - var licenseHash util.Uint256 - if len(licenseHashBytes) == 32 { - copy(licenseHash[:], licenseHashBytes) - } - - // Committee only - if !s.checkCommittee(ic) { - panic(ErrSalusNotCommittee) - } - - // Validate inputs - if len(name) == 0 || len(name) > 128 { - panic(ErrSalusInvalidName) - } - if len(specialty) == 0 || len(specialty) > 64 { - panic(ErrSalusInvalidSpecialty) - } - - // Check if provider already exists - _, exists := s.getProviderIDByAddress(ic.DAO, address) - if exists { - panic(ErrSalusProviderExists) - } - - // Get next provider ID - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - providerID := cache.providerCount - cache.providerCount++ - s.setProviderCounter(ic.DAO, cache.providerCount) - - // Create provider - provider := &state.HealthcareProvider{ - Address: address, - Name: name, - ProviderID: providerID, - Specialty: specialty, - LicenseHash: licenseHash, - Status: state.ProviderStatusActive, - RegisteredAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - - s.putProvider(ic.DAO, provider) - s.setProviderByAddress(ic.DAO, address, providerID) - - // Emit event - ic.AddNotification(s.Hash, ProviderRegisteredEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(providerID))), - stackitem.NewByteArray(address.BytesBE()), - stackitem.NewByteArray([]byte(specialty)), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(providerID))) -} - -// suspendProvider suspends a healthcare provider (committee only). -func (s *Salus) suspendProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { - providerID := toUint64(args[0]) - reason := toString(args[1]) - - // Committee only - if !s.checkCommittee(ic) { - panic(ErrSalusNotCommittee) - } - - provider := s.getProviderInternal(ic.DAO, providerID) - if provider == nil { - panic(ErrSalusProviderNotFound) - } - if provider.Status == state.ProviderStatusRevoked { - panic(ErrSalusProviderRevoked) - } - - // Suspend - provider.Status = state.ProviderStatusSuspended - provider.UpdatedAt = ic.Block.Index - s.putProvider(ic.DAO, provider) - - // Emit event - ic.AddNotification(s.Hash, ProviderSuspendedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(providerID))), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// getProvider returns provider details. -func (s *Salus) getProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { - providerID := toUint64(args[0]) - - provider := s.getProviderInternal(ic.DAO, providerID) - if provider == nil { - return stackitem.Null{} - } - - item, _ := provider.ToStackItem() - return item -} - -// getProviderByAddress returns provider by address. -func (s *Salus) getProviderByAddress(ic *interop.Context, args []stackitem.Item) stackitem.Item { - address := toUint160(args[0]) - - providerID, found := s.getProviderIDByAddress(ic.DAO, address) - if !found { - return stackitem.Null{} - } - - provider := s.getProviderInternal(ic.DAO, providerID) - if provider == nil { - return stackitem.Null{} - } - - item, _ := provider.ToStackItem() - return item -} - -// authorizeAccess grants provider access to patient records. -func (s *Salus) authorizeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - patient := toUint160(args[0]) - provider := toUint160(args[1]) - accessLevel := state.AccessLevel(toUint64(args[2])) - duration := toUint32(args[3]) - - // Get patient's Vita and account - vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) - if !found { - panic(ErrSalusAccountNotFound) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - panic(ErrSalusAccountNotFound) - } - if acc.Status != state.HealthcareAccountActive { - panic(ErrSalusAccountSuspended) - } - - // Check caller is patient (self-authorization) - caller := ic.VM.GetCallingScriptHash() - if caller != patient && !s.checkCommittee(ic) { - panic(ErrSalusNotPatient) - } - - // Check max duration - cfg := s.getConfigInternal(ic.DAO) - if duration > cfg.MaxAuthorizationDuration { - panic(ErrSalusExceedsMaxDuration) - } - - // Check if authorization already exists - existingAuthID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider) - if exists { - existingAuth := s.getAuthInternal(ic.DAO, existingAuthID) - if existingAuth != nil && existingAuth.IsValid(ic.Block.Index) { - panic(ErrSalusAuthorizationExists) - } - } - - // Get next auth ID - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - authID := cache.authorizationCount - cache.authorizationCount++ - s.setAuthCounter(ic.DAO, cache.authorizationCount) - - // Calculate expiry - expiresAt := uint32(0) - if duration > 0 { - expiresAt = ic.Block.Index + duration - } - - // Create authorization - auth := &state.ProviderAuthorization{ - ID: authID, - VitaID: vitaID, - Patient: patient, - Provider: provider, - AccessLevel: accessLevel, - StartsAt: ic.Block.Index, - ExpiresAt: expiresAt, - IsActive: true, - GrantedAt: ic.Block.Index, - } - - s.putAuth(ic.DAO, auth) - s.setActiveAuthID(ic.DAO, vitaID, provider, authID) - - // Emit event - ic.AddNotification(s.Hash, AuthorizationGrantedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(authID))), - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewByteArray(provider.BytesBE()), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(authID))) -} - -// revokeAccess revokes provider access. -func (s *Salus) revokeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - authID := toUint64(args[0]) - - auth := s.getAuthInternal(ic.DAO, authID) - if auth == nil { - panic(ErrSalusAuthorizationNotFound) - } - - // Check caller is patient or committee - caller := ic.VM.GetCallingScriptHash() - if caller != auth.Patient && !s.checkCommittee(ic) { - panic(ErrSalusNotPatient) - } - - // Revoke - auth.IsActive = false - s.putAuth(ic.DAO, auth) - s.clearActiveAuth(ic.DAO, auth.VitaID, auth.Provider) - - // Emit event - ic.AddNotification(s.Hash, AuthorizationRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(authID))), - })) - - return stackitem.NewBool(true) -} - -// getAuthorization returns authorization details. -func (s *Salus) getAuthorization(ic *interop.Context, args []stackitem.Item) stackitem.Item { - authID := toUint64(args[0]) - - auth := s.getAuthInternal(ic.DAO, authID) - if auth == nil { - return stackitem.Null{} - } - - item, _ := auth.ToStackItem() - return item -} - -// hasAccess checks if provider has access to patient. -func (s *Salus) hasAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - patient := toUint160(args[0]) - provider := toUint160(args[1]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) - if !found { - return stackitem.NewBool(false) - } - - authID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider) - if !exists { - return stackitem.NewBool(false) - } - - auth := s.getAuthInternal(ic.DAO, authID) - if auth == nil { - return stackitem.NewBool(false) - } - - return stackitem.NewBool(auth.IsValid(ic.Block.Index)) -} - -// emergencyAccess requests emergency access to patient records. -func (s *Salus) emergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - patient := toUint160(args[0]) - reason := toString(args[1]) - - // Check provider authority - if !s.checkHealthcareProvider(ic) { - panic(ErrSalusNotProvider) - } - - if len(reason) == 0 || len(reason) > 256 { - panic(ErrSalusInvalidReason) - } - - // Get provider address - provider := ic.VM.GetCallingScriptHash() - - // Get patient's Vita - if s.Vita == nil { - panic(ErrSalusNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient) - if err != nil || vita == nil || vita.Status != state.TokenStatusActive { - panic(ErrSalusNoVita) - } - - // Get config for emergency duration - cfg := s.getConfigInternal(ic.DAO) - - // Get next emergency ID - cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) - emergencyID := cache.emergencyCount - cache.emergencyCount++ - s.setEmergencyCounter(ic.DAO, cache.emergencyCount) - - // Create emergency access - emergency := &state.EmergencyAccess{ - ID: emergencyID, - VitaID: vita.TokenID, - Patient: patient, - Provider: provider, - Reason: reason, - GrantedAt: ic.Block.Index, - ExpiresAt: ic.Block.Index + cfg.EmergencyAccessDuration, - WasReviewed: false, - } - - s.putEmergency(ic.DAO, emergency) - - // Emit event - ic.AddNotification(s.Hash, EmergencyAccessGrantedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(emergencyID))), - stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), - stackitem.NewByteArray(provider.BytesBE()), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(emergencyID))) -} - -// reviewEmergencyAccess marks emergency access as reviewed (committee only). -func (s *Salus) reviewEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - emergencyID := toUint64(args[0]) - - // Committee only - if !s.checkCommittee(ic) { - panic(ErrSalusNotCommittee) - } - - emergency := s.getEmergencyInternal(ic.DAO, emergencyID) - if emergency == nil { - panic(ErrSalusEmergencyNotFound) - } - - // Mark as reviewed - emergency.WasReviewed = true - s.putEmergency(ic.DAO, emergency) - - // Emit event - ic.AddNotification(s.Hash, EmergencyAccessReviewedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(emergencyID))), - })) - - return stackitem.NewBool(true) -} - -// getEmergencyAccess returns emergency access details. -func (s *Salus) getEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { - emergencyID := toUint64(args[0]) - - emergency := s.getEmergencyInternal(ic.DAO, emergencyID) - if emergency == nil { - return stackitem.Null{} - } - - item, _ := emergency.ToStackItem() - return item -} - -// getConfig returns the Salus configuration. -func (s *Salus) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cfg := s.getConfigInternal(ic.DAO) - item, _ := cfg.ToStackItem() - return item -} - -// getTotalAccounts returns the total number of healthcare accounts. -func (s *Salus) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*SalusCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount))) -} - -// getTotalRecords returns the total number of medical records. -func (s *Salus) getTotalRecords(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*SalusCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.recordCount))) -} - -// getTotalProviders returns the total number of healthcare providers. -func (s *Salus) getTotalProviders(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*SalusCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.providerCount))) -} - -// ===== Public Interface Methods for Cross-Contract Access ===== - -// GetAccountByOwner returns a healthcare account by owner address. -func (s *Salus) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.HealthcareAccount, error) { - vitaID, found := s.getVitaIDByOwner(d, owner) - if !found { - return nil, ErrSalusAccountNotFound - } - acc := s.getAccountInternal(d, vitaID) - if acc == nil { - return nil, ErrSalusAccountNotFound - } - return acc, nil -} - -// HasValidAuthorization checks if provider has valid authorization for patient. -func (s *Salus) HasValidAuthorization(d *dao.Simple, patient util.Uint160, provider util.Uint160, blockHeight uint32) bool { - vitaID, found := s.getVitaIDByOwner(d, patient) - if !found { - return false - } - - authID, exists := s.getActiveAuthID(d, vitaID, provider) - if !exists { - return false - } - - auth := s.getAuthInternal(d, authID) - if auth == nil { - return false - } - - return auth.IsValid(blockHeight) -} - -// Address returns the contract's script hash. -func (s *Salus) Address() util.Uint160 { - return s.Hash -} +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/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" +) + +// Salus represents the universal healthcare native contract. +type Salus struct { + interop.ContractMD + Tutus ITutus + Vita IVita + RoleRegistry IRoleRegistry + Lex ILex +} + +// SalusCache represents the cached state for Salus contract. +type SalusCache struct { + accountCount uint64 + recordCount uint64 + providerCount uint64 + authorizationCount uint64 + emergencyCount uint64 +} + +// Storage key prefixes for Salus. +const ( + salusPrefixAccount byte = 0x01 // vitaID -> HealthcareAccount + salusPrefixAccountByOwner byte = 0x02 // owner -> vitaID + salusPrefixRecord byte = 0x10 // recordID -> MedicalRecord + salusPrefixRecordByPatient byte = 0x11 // vitaID + recordID -> exists + salusPrefixRecordByProvider byte = 0x12 // provider + recordID -> exists + salusPrefixProvider byte = 0x20 // providerID -> HealthcareProvider + salusPrefixProviderByAddress byte = 0x21 // provider address -> providerID + salusPrefixAuthorization byte = 0x30 // authID -> ProviderAuthorization + salusPrefixAuthByPatient byte = 0x31 // vitaID + authID -> exists + salusPrefixAuthByProvider byte = 0x32 // provider + authID -> exists + salusPrefixActiveAuth byte = 0x33 // vitaID + provider -> authID + salusPrefixEmergencyAccess byte = 0x40 // emergencyID -> EmergencyAccess + salusPrefixEmergencyByPatient byte = 0x41 // vitaID + emergencyID -> exists + salusPrefixAccountCounter byte = 0xF0 // -> uint64 + salusPrefixRecordCounter byte = 0xF1 // -> next record ID + salusPrefixProviderCounter byte = 0xF2 // -> next provider ID + salusPrefixAuthCounter byte = 0xF3 // -> next authorization ID + salusPrefixEmergencyCounter byte = 0xF4 // -> next emergency access ID + salusPrefixConfig byte = 0xFF // -> SalusConfig +) + +// Event names for Salus. +const ( + HealthcareActivatedEvent = "HealthcareActivated" + CreditsAllocatedEventSalus = "CreditsAllocated" + MedicalRecordCreatedEvent = "MedicalRecordCreated" + ProviderRegisteredEvent = "ProviderRegistered" + ProviderSuspendedEvent = "ProviderSuspended" + AuthorizationGrantedEvent = "AuthorizationGranted" + AuthorizationRevokedEvent = "AuthorizationRevoked" + EmergencyAccessGrantedEvent = "EmergencyAccessGranted" + EmergencyAccessReviewedEvent = "EmergencyAccessReviewed" +) + +// Role constants for healthcare providers. +const ( + RoleHealthcare uint64 = 21 // Can record medical events and access authorized records +) + +// Various errors for Salus. +var ( + ErrSalusAccountNotFound = errors.New("healthcare account not found") + ErrSalusAccountExists = errors.New("healthcare account already exists") + ErrSalusAccountSuspended = errors.New("healthcare account is suspended") + ErrSalusAccountClosed = errors.New("healthcare account is closed") + ErrSalusNoVita = errors.New("owner must have an active Vita") + ErrSalusInsufficientCredits = errors.New("insufficient healthcare credits") + ErrSalusInvalidCredits = errors.New("invalid credit amount") + ErrSalusRecordNotFound = errors.New("medical record not found") + ErrSalusProviderNotFound = errors.New("healthcare provider not found") + ErrSalusProviderExists = errors.New("healthcare provider already registered") + ErrSalusProviderSuspended = errors.New("healthcare provider is suspended") + ErrSalusProviderRevoked = errors.New("healthcare provider is revoked") + ErrSalusNotProvider = errors.New("caller is not an authorized healthcare provider") + ErrSalusNotCommittee = errors.New("invalid committee signature") + ErrSalusInvalidOwner = errors.New("invalid owner address") + ErrSalusInvalidProvider = errors.New("invalid provider address") + ErrSalusAuthorizationNotFound = errors.New("authorization not found") + ErrSalusAuthorizationExpired = errors.New("authorization has expired") + ErrSalusAuthorizationExists = errors.New("authorization already exists") + ErrSalusNotPatient = errors.New("caller is not the patient") + ErrSalusHealthcareRestricted = errors.New("healthcare right is restricted") + ErrSalusEmergencyNotFound = errors.New("emergency access not found") + ErrSalusInvalidReason = errors.New("invalid reason") + ErrSalusInvalidName = errors.New("invalid name") + ErrSalusInvalidSpecialty = errors.New("invalid specialty") + ErrSalusNoAccess = errors.New("no access to patient records") + ErrSalusExceedsMaxDuration = errors.New("exceeds maximum authorization duration") +) + +var ( + _ interop.Contract = (*Salus)(nil) + _ dao.NativeContractCache = (*SalusCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *SalusCache) Copy() dao.NativeContractCache { + return &SalusCache{ + accountCount: c.accountCount, + recordCount: c.recordCount, + providerCount: c.providerCount, + authorizationCount: c.authorizationCount, + emergencyCount: c.emergencyCount, + } +} + +// checkCommittee checks if the caller has committee authority. +func (s *Salus) checkCommittee(ic *interop.Context) bool { + if s.RoleRegistry != nil { + return s.RoleRegistry.CheckCommittee(ic) + } + return s.Tutus.CheckCommittee(ic) +} + +// checkHealthcareProvider checks if the caller has healthcare provider authority. +func (s *Salus) checkHealthcareProvider(ic *interop.Context) bool { + caller := ic.VM.GetCallingScriptHash() + if s.RoleRegistry != nil { + if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleHealthcare, ic.Block.Index) { + return true + } + } + // Committee members can also act as healthcare providers + return s.checkCommittee(ic) +} + +// checkHealthcareRight checks if subject has healthcare rights via Lex. +func (s *Salus) checkHealthcareRight(ic *interop.Context, subject util.Uint160) bool { + if s.Lex == nil { + return true // Allow if Lex not available + } + return s.Lex.HasRightInternal(ic.DAO, subject, state.RightHealthcare, ic.Block.Index) +} + +// newSalus creates a new Salus native contract. +func newSalus() *Salus { + s := &Salus{ + ContractMD: *interop.NewContractMD(nativenames.Salus, nativeids.Salus), + } + defer s.BuildHFSpecificMD(s.ActiveIn()) + + // ===== Account Management ===== + + // activateHealthcare - Activate healthcare account for a Vita holder + desc := NewDescriptor("activateHealthcare", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md := NewMethodAndPrice(s.activateHealthcare, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getAccount - Get healthcare account by owner + desc = NewDescriptor("getAccount", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getAccountByVitaID - Get account by Vita ID + desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, + manifest.NewParameter("vitaID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // allocateCredits - Allocate healthcare credits (committee only) + desc = NewDescriptor("allocateCredits", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getCredits - Get available credits + desc = NewDescriptor("getCredits", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Medical Records ===== + + // recordMedicalEvent - Record a medical event (provider only) + desc = NewDescriptor("recordMedicalEvent", smartcontract.IntegerType, + manifest.NewParameter("patient", smartcontract.Hash160Type), + manifest.NewParameter("recordType", smartcontract.IntegerType), + manifest.NewParameter("contentHash", smartcontract.Hash256Type), + manifest.NewParameter("credits", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.recordMedicalEvent, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getMedicalRecord - Get medical record by ID + desc = NewDescriptor("getMedicalRecord", smartcontract.ArrayType, + manifest.NewParameter("recordID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getMedicalRecord, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Provider Management ===== + + // registerProvider - Register a healthcare provider (committee only) + desc = NewDescriptor("registerProvider", smartcontract.IntegerType, + manifest.NewParameter("address", smartcontract.Hash160Type), + manifest.NewParameter("name", smartcontract.StringType), + manifest.NewParameter("specialty", smartcontract.StringType), + manifest.NewParameter("licenseHash", smartcontract.Hash256Type)) + md = NewMethodAndPrice(s.registerProvider, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // suspendProvider - Suspend a healthcare provider (committee only) + desc = NewDescriptor("suspendProvider", smartcontract.BoolType, + manifest.NewParameter("providerID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.suspendProvider, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getProvider - Get provider details + desc = NewDescriptor("getProvider", smartcontract.ArrayType, + manifest.NewParameter("providerID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getProvider, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getProviderByAddress - Get provider by address + desc = NewDescriptor("getProviderByAddress", smartcontract.ArrayType, + manifest.NewParameter("address", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getProviderByAddress, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Authorization Management ===== + + // authorizeAccess - Grant provider access to patient records + desc = NewDescriptor("authorizeAccess", smartcontract.IntegerType, + manifest.NewParameter("patient", smartcontract.Hash160Type), + manifest.NewParameter("provider", smartcontract.Hash160Type), + manifest.NewParameter("accessLevel", smartcontract.IntegerType), + manifest.NewParameter("duration", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.authorizeAccess, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // revokeAccess - Revoke provider access + desc = NewDescriptor("revokeAccess", smartcontract.BoolType, + manifest.NewParameter("authID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.revokeAccess, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getAuthorization - Get authorization details + desc = NewDescriptor("getAuthorization", smartcontract.ArrayType, + manifest.NewParameter("authID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getAuthorization, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // hasAccess - Check if provider has access to patient + desc = NewDescriptor("hasAccess", smartcontract.BoolType, + manifest.NewParameter("patient", smartcontract.Hash160Type), + manifest.NewParameter("provider", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.hasAccess, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Emergency Access ===== + + // emergencyAccess - Request emergency access (provider only) + desc = NewDescriptor("emergencyAccess", smartcontract.IntegerType, + manifest.NewParameter("patient", smartcontract.Hash160Type), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.emergencyAccess, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // reviewEmergencyAccess - Review emergency access (committee only) + desc = NewDescriptor("reviewEmergencyAccess", smartcontract.BoolType, + manifest.NewParameter("emergencyID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.reviewEmergencyAccess, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getEmergencyAccess - Get emergency access details + desc = NewDescriptor("getEmergencyAccess", smartcontract.ArrayType, + manifest.NewParameter("emergencyID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getEmergencyAccess, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Query Methods ===== + + // getConfig - Get Salus configuration + desc = NewDescriptor("getConfig", smartcontract.ArrayType) + md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalAccounts - Get total healthcare accounts + desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalRecords - Get total medical records + desc = NewDescriptor("getTotalRecords", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalRecords, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalProviders - Get total healthcare providers + desc = NewDescriptor("getTotalProviders", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalProviders, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Events ===== + + // HealthcareActivated event + eDesc := NewEventDescriptor(HealthcareActivatedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("owner", smartcontract.Hash160Type)) + s.AddEvent(NewEvent(eDesc)) + + // CreditsAllocated event + eDesc = NewEventDescriptor(CreditsAllocatedEventSalus, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("total", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + // MedicalRecordCreated event + eDesc = NewEventDescriptor(MedicalRecordCreatedEvent, + manifest.NewParameter("recordID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("recordType", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + // ProviderRegistered event + eDesc = NewEventDescriptor(ProviderRegisteredEvent, + manifest.NewParameter("providerID", smartcontract.IntegerType), + manifest.NewParameter("address", smartcontract.Hash160Type), + manifest.NewParameter("specialty", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // ProviderSuspended event + eDesc = NewEventDescriptor(ProviderSuspendedEvent, + manifest.NewParameter("providerID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // AuthorizationGranted event + eDesc = NewEventDescriptor(AuthorizationGrantedEvent, + manifest.NewParameter("authID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("provider", smartcontract.Hash160Type)) + s.AddEvent(NewEvent(eDesc)) + + // AuthorizationRevoked event + eDesc = NewEventDescriptor(AuthorizationRevokedEvent, + manifest.NewParameter("authID", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + // EmergencyAccessGranted event + eDesc = NewEventDescriptor(EmergencyAccessGrantedEvent, + manifest.NewParameter("emergencyID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("provider", smartcontract.Hash160Type)) + s.AddEvent(NewEvent(eDesc)) + + // EmergencyAccessReviewed event + eDesc = NewEventDescriptor(EmergencyAccessReviewedEvent, + manifest.NewParameter("emergencyID", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + return s +} + +// Metadata returns contract metadata. +func (s *Salus) Metadata() *interop.ContractMD { + return &s.ContractMD +} + +// Initialize initializes the Salus contract. +func (s *Salus) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != s.ActiveIn() { + return nil + } + + // Initialize counters + s.setAccountCounter(ic.DAO, 0) + s.setRecordCounter(ic.DAO, 0) + s.setProviderCounter(ic.DAO, 0) + s.setAuthCounter(ic.DAO, 0) + s.setEmergencyCounter(ic.DAO, 0) + + // Initialize config with defaults + cfg := &state.SalusConfig{ + DefaultAnnualCredits: 10000, // 10000 healthcare credits per year + EmergencyAccessDuration: 86400, // ~24 hours (1-second blocks) + PreventiveCareBonus: 500, // Bonus for preventive care visits + MaxAuthorizationDuration: 2592000, // ~30 days (1-second blocks) + } + s.setConfig(ic.DAO, cfg) + + // Initialize cache + cache := &SalusCache{ + accountCount: 0, + recordCount: 0, + providerCount: 0, + authorizationCount: 0, + emergencyCount: 0, + } + ic.DAO.SetCache(s.ID, cache) + + return nil +} + +// InitializeCache initializes the cache from storage. +func (s *Salus) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + cache := &SalusCache{ + accountCount: s.getAccountCounter(d), + recordCount: s.getRecordCounter(d), + providerCount: s.getProviderCounter(d), + authorizationCount: s.getAuthCounter(d), + emergencyCount: s.getEmergencyCounter(d), + } + d.SetCache(s.ID, cache) + return nil +} + +// OnPersist is called before block is committed. +func (s *Salus) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist is called after block is committed. +func (s *Salus) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn returns the hardfork at which this contract is activated. +func (s *Salus) ActiveIn() *config.Hardfork { + return nil // Always active +} + +// ===== Storage Helpers ===== + +func (s *Salus) makeAccountKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = salusPrefixAccount + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func (s *Salus) makeAccountByOwnerKey(owner util.Uint160) []byte { + key := make([]byte, 21) + key[0] = salusPrefixAccountByOwner + copy(key[1:], owner.BytesBE()) + return key +} + +func (s *Salus) makeRecordKey(recordID uint64) []byte { + key := make([]byte, 9) + key[0] = salusPrefixRecord + binary.BigEndian.PutUint64(key[1:], recordID) + return key +} + +func (s *Salus) makeRecordByPatientKey(vitaID, recordID uint64) []byte { + key := make([]byte, 17) + key[0] = salusPrefixRecordByPatient + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], recordID) + return key +} + +func (s *Salus) makeProviderKey(providerID uint64) []byte { + key := make([]byte, 9) + key[0] = salusPrefixProvider + binary.BigEndian.PutUint64(key[1:], providerID) + return key +} + +func (s *Salus) makeProviderByAddressKey(address util.Uint160) []byte { + key := make([]byte, 21) + key[0] = salusPrefixProviderByAddress + copy(key[1:], address.BytesBE()) + return key +} + +func (s *Salus) makeAuthorizationKey(authID uint64) []byte { + key := make([]byte, 9) + key[0] = salusPrefixAuthorization + binary.BigEndian.PutUint64(key[1:], authID) + return key +} + +func (s *Salus) makeActiveAuthKey(vitaID uint64, provider util.Uint160) []byte { + key := make([]byte, 29) + key[0] = salusPrefixActiveAuth + binary.BigEndian.PutUint64(key[1:9], vitaID) + copy(key[9:], provider.BytesBE()) + return key +} + +func (s *Salus) makeEmergencyKey(emergencyID uint64) []byte { + key := make([]byte, 9) + key[0] = salusPrefixEmergencyAccess + binary.BigEndian.PutUint64(key[1:], emergencyID) + return key +} + +// Counter getters/setters +func (s *Salus) getAccountCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{salusPrefixAccountCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Salus) setAccountCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{salusPrefixAccountCounter}, buf) +} + +func (s *Salus) getRecordCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{salusPrefixRecordCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Salus) setRecordCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{salusPrefixRecordCounter}, buf) +} + +func (s *Salus) getProviderCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{salusPrefixProviderCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Salus) setProviderCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{salusPrefixProviderCounter}, buf) +} + +func (s *Salus) getAuthCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{salusPrefixAuthCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Salus) setAuthCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{salusPrefixAuthCounter}, buf) +} + +func (s *Salus) getEmergencyCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{salusPrefixEmergencyCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Salus) setEmergencyCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{salusPrefixEmergencyCounter}, buf) +} + +// Config getter/setter +func (s *Salus) getConfigInternal(d *dao.Simple) *state.SalusConfig { + si := d.GetStorageItem(s.ID, []byte{salusPrefixConfig}) + if si == nil { + return &state.SalusConfig{ + DefaultAnnualCredits: 10000, + EmergencyAccessDuration: 86400, + PreventiveCareBonus: 500, + MaxAuthorizationDuration: 2592000, + } + } + cfg := new(state.SalusConfig) + item, _ := stackitem.Deserialize(si) + cfg.FromStackItem(item) + return cfg +} + +func (s *Salus) setConfig(d *dao.Simple, cfg *state.SalusConfig) { + item, _ := cfg.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, []byte{salusPrefixConfig}, data) +} + +// Account storage +func (s *Salus) getAccountInternal(d *dao.Simple, vitaID uint64) *state.HealthcareAccount { + si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID)) + if si == nil { + return nil + } + acc := new(state.HealthcareAccount) + item, _ := stackitem.Deserialize(si) + acc.FromStackItem(item) + return acc +} + +func (s *Salus) putAccount(d *dao.Simple, acc *state.HealthcareAccount) { + item, _ := acc.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data) +} + +func (s *Salus) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { + si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (s *Salus) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, vitaID) + d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf) +} + +// Record storage +func (s *Salus) getRecordInternal(d *dao.Simple, recordID uint64) *state.MedicalRecord { + si := d.GetStorageItem(s.ID, s.makeRecordKey(recordID)) + if si == nil { + return nil + } + record := new(state.MedicalRecord) + item, _ := stackitem.Deserialize(si) + record.FromStackItem(item) + return record +} + +func (s *Salus) putRecord(d *dao.Simple, record *state.MedicalRecord) { + item, _ := record.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeRecordKey(record.ID), data) +} + +func (s *Salus) setRecordByPatient(d *dao.Simple, vitaID, recordID uint64) { + d.PutStorageItem(s.ID, s.makeRecordByPatientKey(vitaID, recordID), []byte{1}) +} + +// Provider storage +func (s *Salus) getProviderInternal(d *dao.Simple, providerID uint64) *state.HealthcareProvider { + si := d.GetStorageItem(s.ID, s.makeProviderKey(providerID)) + if si == nil { + return nil + } + provider := new(state.HealthcareProvider) + item, _ := stackitem.Deserialize(si) + provider.FromStackItem(item) + return provider +} + +func (s *Salus) putProvider(d *dao.Simple, provider *state.HealthcareProvider) { + item, _ := provider.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeProviderKey(provider.ProviderID), data) +} + +func (s *Salus) getProviderIDByAddress(d *dao.Simple, address util.Uint160) (uint64, bool) { + si := d.GetStorageItem(s.ID, s.makeProviderByAddressKey(address)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (s *Salus) setProviderByAddress(d *dao.Simple, address util.Uint160, providerID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, providerID) + d.PutStorageItem(s.ID, s.makeProviderByAddressKey(address), buf) +} + +// Authorization storage +func (s *Salus) getAuthInternal(d *dao.Simple, authID uint64) *state.ProviderAuthorization { + si := d.GetStorageItem(s.ID, s.makeAuthorizationKey(authID)) + if si == nil { + return nil + } + auth := new(state.ProviderAuthorization) + item, _ := stackitem.Deserialize(si) + auth.FromStackItem(item) + return auth +} + +func (s *Salus) putAuth(d *dao.Simple, auth *state.ProviderAuthorization) { + item, _ := auth.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeAuthorizationKey(auth.ID), data) +} + +func (s *Salus) getActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160) (uint64, bool) { + si := d.GetStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (s *Salus) setActiveAuthID(d *dao.Simple, vitaID uint64, provider util.Uint160, authID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, authID) + d.PutStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider), buf) +} + +func (s *Salus) clearActiveAuth(d *dao.Simple, vitaID uint64, provider util.Uint160) { + d.DeleteStorageItem(s.ID, s.makeActiveAuthKey(vitaID, provider)) +} + +// Emergency access storage +func (s *Salus) getEmergencyInternal(d *dao.Simple, emergencyID uint64) *state.EmergencyAccess { + si := d.GetStorageItem(s.ID, s.makeEmergencyKey(emergencyID)) + if si == nil { + return nil + } + emergency := new(state.EmergencyAccess) + item, _ := stackitem.Deserialize(si) + emergency.FromStackItem(item) + return emergency +} + +func (s *Salus) putEmergency(d *dao.Simple, emergency *state.EmergencyAccess) { + item, _ := emergency.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeEmergencyKey(emergency.ID), data) +} + +// ===== Contract Methods ===== + +// activateHealthcare activates healthcare account for a Vita holder. +func (s *Salus) activateHealthcare(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + // Check owner has active Vita + if s.Vita == nil { + panic(ErrSalusNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil { + panic(ErrSalusNoVita) + } + if vita.Status != state.TokenStatusActive { + panic(ErrSalusNoVita) + } + + // Check if account already exists + existing := s.getAccountInternal(ic.DAO, vita.TokenID) + if existing != nil { + panic(ErrSalusAccountExists) + } + + // Check healthcare rights + if !s.checkHealthcareRight(ic, owner) { + // Log but allow (EnforcementLogging) + } + + // Get cache and increment counter + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + cache.accountCount++ + s.setAccountCounter(ic.DAO, cache.accountCount) + + // Get default credits from config + cfg := s.getConfigInternal(ic.DAO) + + // Create account + acc := &state.HealthcareAccount{ + VitaID: vita.TokenID, + Owner: owner, + AnnualAllocation: cfg.DefaultAnnualCredits, + CreditsUsed: 0, + CreditsAvailable: cfg.DefaultAnnualCredits, + BiologicalAge: 0, + LastCheckup: 0, + Status: state.HealthcareAccountActive, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + + // Store account + s.putAccount(ic.DAO, acc) + s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID) + + // Emit event + ic.AddNotification(s.Hash, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), + stackitem.NewByteArray(owner.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +// getAccount returns healthcare account by owner. +func (s *Salus) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.Null{} + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.Null{} + } + + item, _ := acc.ToStackItem() + return item +} + +// getAccountByVitaID returns healthcare account by Vita ID. +func (s *Salus) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { + vitaID := toUint64(args[0]) + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.Null{} + } + + item, _ := acc.ToStackItem() + return item +} + +// allocateCredits allocates healthcare credits to an account (committee only). +func (s *Salus) allocateCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + amount := toUint64(args[1]) + // reason := toString(args[2]) // for logging + + // Committee only + if !s.checkCommittee(ic) { + panic(ErrSalusNotCommittee) + } + + if amount == 0 { + panic(ErrSalusInvalidCredits) + } + + // Get or create account + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + // Auto-create account if Vita exists + if s.Vita == nil { + panic(ErrSalusNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil || vita.Status != state.TokenStatusActive { + panic(ErrSalusNoVita) + } + vitaID = vita.TokenID + + cfg := s.getConfigInternal(ic.DAO) + + // Create account + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + cache.accountCount++ + s.setAccountCounter(ic.DAO, cache.accountCount) + + acc := &state.HealthcareAccount{ + VitaID: vitaID, + Owner: owner, + AnnualAllocation: cfg.DefaultAnnualCredits, + CreditsUsed: 0, + CreditsAvailable: amount, + BiologicalAge: 0, + LastCheckup: 0, + Status: state.HealthcareAccountActive, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + s.putAccount(ic.DAO, acc) + s.setOwnerToVitaID(ic.DAO, owner, vitaID) + + // Emit events + ic.AddNotification(s.Hash, HealthcareActivatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewByteArray(owner.BytesBE()), + })) + ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + })) + + return stackitem.NewBool(true) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + panic(ErrSalusAccountNotFound) + } + if acc.Status != state.HealthcareAccountActive { + panic(ErrSalusAccountSuspended) + } + + // Add credits + acc.CreditsAvailable += amount + acc.UpdatedAt = ic.Block.Index + + s.putAccount(ic.DAO, acc) + + // Emit event + ic.AddNotification(s.Hash, CreditsAllocatedEventSalus, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + stackitem.NewBigInteger(big.NewInt(int64(acc.CreditsAvailable))), + })) + + return stackitem.NewBool(true) +} + +// getCredits returns available credits for an owner. +func (s *Salus) getCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + return stackitem.NewBigInteger(big.NewInt(int64(acc.CreditsAvailable))) +} + +// recordMedicalEvent records a medical event (provider only). +func (s *Salus) recordMedicalEvent(ic *interop.Context, args []stackitem.Item) stackitem.Item { + patient := toUint160(args[0]) + recordType := state.MedicalRecordType(toUint64(args[1])) + contentHashBytes := toBytes(args[2]) + credits := toUint64(args[3]) + + // Convert bytes to Uint256 + var contentHash util.Uint256 + if len(contentHashBytes) == 32 { + copy(contentHash[:], contentHashBytes) + } + + // Check provider authority + if !s.checkHealthcareProvider(ic) { + panic(ErrSalusNotProvider) + } + + // Get provider address + provider := ic.VM.GetCallingScriptHash() + + // Get patient's Vita + if s.Vita == nil { + panic(ErrSalusNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient) + if err != nil || vita == nil || vita.Status != state.TokenStatusActive { + panic(ErrSalusNoVita) + } + + // Check provider has access (unless emergency or authorized) + vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) + if found { + authID, hasAuth := s.getActiveAuthID(ic.DAO, vitaID, provider) + if hasAuth { + auth := s.getAuthInternal(ic.DAO, authID) + if auth == nil || !auth.IsValid(ic.Block.Index) { + panic(ErrSalusNoAccess) + } + } + // If no auth, still allow (emergency can be logged separately) + } + + // Get or auto-create account + if !found { + // Auto-create + cfg := s.getConfigInternal(ic.DAO) + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + cache.accountCount++ + s.setAccountCounter(ic.DAO, cache.accountCount) + + acc := &state.HealthcareAccount{ + VitaID: vita.TokenID, + Owner: patient, + AnnualAllocation: cfg.DefaultAnnualCredits, + CreditsUsed: 0, + CreditsAvailable: cfg.DefaultAnnualCredits, + BiologicalAge: 0, + LastCheckup: 0, + Status: state.HealthcareAccountActive, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + s.putAccount(ic.DAO, acc) + s.setOwnerToVitaID(ic.DAO, patient, vita.TokenID) + vitaID = vita.TokenID + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + panic(ErrSalusAccountNotFound) + } + if acc.Status != state.HealthcareAccountActive { + panic(ErrSalusAccountSuspended) + } + + // Check sufficient credits + if acc.CreditsAvailable < credits { + panic(ErrSalusInsufficientCredits) + } + + // Get next record ID + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + recordID := cache.recordCount + cache.recordCount++ + s.setRecordCounter(ic.DAO, cache.recordCount) + + // Deduct credits + acc.CreditsUsed += credits + acc.CreditsAvailable -= credits + acc.UpdatedAt = ic.Block.Index + + // Update last checkup if appropriate + if recordType == state.RecordTypeCheckup || recordType == state.RecordTypePreventive { + acc.LastCheckup = ic.Block.Index + } + + s.putAccount(ic.DAO, acc) + + // Create record + record := &state.MedicalRecord{ + ID: recordID, + VitaID: vitaID, + Patient: patient, + Provider: provider, + RecordType: recordType, + ContentHash: contentHash, + CreditsUsed: credits, + CreatedAt: ic.Block.Index, + IsActive: true, + } + + s.putRecord(ic.DAO, record) + s.setRecordByPatient(ic.DAO, vitaID, recordID) + + // Emit event + ic.AddNotification(s.Hash, MedicalRecordCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(recordID))), + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewBigInteger(big.NewInt(int64(recordType))), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(recordID))) +} + +// getMedicalRecord returns medical record by ID. +func (s *Salus) getMedicalRecord(ic *interop.Context, args []stackitem.Item) stackitem.Item { + recordID := toUint64(args[0]) + + record := s.getRecordInternal(ic.DAO, recordID) + if record == nil { + return stackitem.Null{} + } + + item, _ := record.ToStackItem() + return item +} + +// registerProvider registers a healthcare provider (committee only). +func (s *Salus) registerProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { + address := toUint160(args[0]) + name := toString(args[1]) + specialty := toString(args[2]) + licenseHashBytes := toBytes(args[3]) + + // Convert bytes to Uint256 + var licenseHash util.Uint256 + if len(licenseHashBytes) == 32 { + copy(licenseHash[:], licenseHashBytes) + } + + // Committee only + if !s.checkCommittee(ic) { + panic(ErrSalusNotCommittee) + } + + // Validate inputs + if len(name) == 0 || len(name) > 128 { + panic(ErrSalusInvalidName) + } + if len(specialty) == 0 || len(specialty) > 64 { + panic(ErrSalusInvalidSpecialty) + } + + // Check if provider already exists + _, exists := s.getProviderIDByAddress(ic.DAO, address) + if exists { + panic(ErrSalusProviderExists) + } + + // Get next provider ID + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + providerID := cache.providerCount + cache.providerCount++ + s.setProviderCounter(ic.DAO, cache.providerCount) + + // Create provider + provider := &state.HealthcareProvider{ + Address: address, + Name: name, + ProviderID: providerID, + Specialty: specialty, + LicenseHash: licenseHash, + Status: state.ProviderStatusActive, + RegisteredAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + + s.putProvider(ic.DAO, provider) + s.setProviderByAddress(ic.DAO, address, providerID) + + // Emit event + ic.AddNotification(s.Hash, ProviderRegisteredEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(providerID))), + stackitem.NewByteArray(address.BytesBE()), + stackitem.NewByteArray([]byte(specialty)), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(providerID))) +} + +// suspendProvider suspends a healthcare provider (committee only). +func (s *Salus) suspendProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { + providerID := toUint64(args[0]) + reason := toString(args[1]) + + // Committee only + if !s.checkCommittee(ic) { + panic(ErrSalusNotCommittee) + } + + provider := s.getProviderInternal(ic.DAO, providerID) + if provider == nil { + panic(ErrSalusProviderNotFound) + } + if provider.Status == state.ProviderStatusRevoked { + panic(ErrSalusProviderRevoked) + } + + // Suspend + provider.Status = state.ProviderStatusSuspended + provider.UpdatedAt = ic.Block.Index + s.putProvider(ic.DAO, provider) + + // Emit event + ic.AddNotification(s.Hash, ProviderSuspendedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(providerID))), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// getProvider returns provider details. +func (s *Salus) getProvider(ic *interop.Context, args []stackitem.Item) stackitem.Item { + providerID := toUint64(args[0]) + + provider := s.getProviderInternal(ic.DAO, providerID) + if provider == nil { + return stackitem.Null{} + } + + item, _ := provider.ToStackItem() + return item +} + +// getProviderByAddress returns provider by address. +func (s *Salus) getProviderByAddress(ic *interop.Context, args []stackitem.Item) stackitem.Item { + address := toUint160(args[0]) + + providerID, found := s.getProviderIDByAddress(ic.DAO, address) + if !found { + return stackitem.Null{} + } + + provider := s.getProviderInternal(ic.DAO, providerID) + if provider == nil { + return stackitem.Null{} + } + + item, _ := provider.ToStackItem() + return item +} + +// authorizeAccess grants provider access to patient records. +func (s *Salus) authorizeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + patient := toUint160(args[0]) + provider := toUint160(args[1]) + accessLevel := state.AccessLevel(toUint64(args[2])) + duration := toUint32(args[3]) + + // Get patient's Vita and account + vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) + if !found { + panic(ErrSalusAccountNotFound) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + panic(ErrSalusAccountNotFound) + } + if acc.Status != state.HealthcareAccountActive { + panic(ErrSalusAccountSuspended) + } + + // Check caller is patient (self-authorization) + caller := ic.VM.GetCallingScriptHash() + if caller != patient && !s.checkCommittee(ic) { + panic(ErrSalusNotPatient) + } + + // Check max duration + cfg := s.getConfigInternal(ic.DAO) + if duration > cfg.MaxAuthorizationDuration { + panic(ErrSalusExceedsMaxDuration) + } + + // Check if authorization already exists + existingAuthID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider) + if exists { + existingAuth := s.getAuthInternal(ic.DAO, existingAuthID) + if existingAuth != nil && existingAuth.IsValid(ic.Block.Index) { + panic(ErrSalusAuthorizationExists) + } + } + + // Get next auth ID + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + authID := cache.authorizationCount + cache.authorizationCount++ + s.setAuthCounter(ic.DAO, cache.authorizationCount) + + // Calculate expiry + expiresAt := uint32(0) + if duration > 0 { + expiresAt = ic.Block.Index + duration + } + + // Create authorization + auth := &state.ProviderAuthorization{ + ID: authID, + VitaID: vitaID, + Patient: patient, + Provider: provider, + AccessLevel: accessLevel, + StartsAt: ic.Block.Index, + ExpiresAt: expiresAt, + IsActive: true, + GrantedAt: ic.Block.Index, + } + + s.putAuth(ic.DAO, auth) + s.setActiveAuthID(ic.DAO, vitaID, provider, authID) + + // Emit event + ic.AddNotification(s.Hash, AuthorizationGrantedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(authID))), + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewByteArray(provider.BytesBE()), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(authID))) +} + +// revokeAccess revokes provider access. +func (s *Salus) revokeAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + authID := toUint64(args[0]) + + auth := s.getAuthInternal(ic.DAO, authID) + if auth == nil { + panic(ErrSalusAuthorizationNotFound) + } + + // Check caller is patient or committee + caller := ic.VM.GetCallingScriptHash() + if caller != auth.Patient && !s.checkCommittee(ic) { + panic(ErrSalusNotPatient) + } + + // Revoke + auth.IsActive = false + s.putAuth(ic.DAO, auth) + s.clearActiveAuth(ic.DAO, auth.VitaID, auth.Provider) + + // Emit event + ic.AddNotification(s.Hash, AuthorizationRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(authID))), + })) + + return stackitem.NewBool(true) +} + +// getAuthorization returns authorization details. +func (s *Salus) getAuthorization(ic *interop.Context, args []stackitem.Item) stackitem.Item { + authID := toUint64(args[0]) + + auth := s.getAuthInternal(ic.DAO, authID) + if auth == nil { + return stackitem.Null{} + } + + item, _ := auth.ToStackItem() + return item +} + +// hasAccess checks if provider has access to patient. +func (s *Salus) hasAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + patient := toUint160(args[0]) + provider := toUint160(args[1]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, patient) + if !found { + return stackitem.NewBool(false) + } + + authID, exists := s.getActiveAuthID(ic.DAO, vitaID, provider) + if !exists { + return stackitem.NewBool(false) + } + + auth := s.getAuthInternal(ic.DAO, authID) + if auth == nil { + return stackitem.NewBool(false) + } + + return stackitem.NewBool(auth.IsValid(ic.Block.Index)) +} + +// emergencyAccess requests emergency access to patient records. +func (s *Salus) emergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + patient := toUint160(args[0]) + reason := toString(args[1]) + + // Check provider authority + if !s.checkHealthcareProvider(ic) { + panic(ErrSalusNotProvider) + } + + if len(reason) == 0 || len(reason) > 256 { + panic(ErrSalusInvalidReason) + } + + // Get provider address + provider := ic.VM.GetCallingScriptHash() + + // Get patient's Vita + if s.Vita == nil { + panic(ErrSalusNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, patient) + if err != nil || vita == nil || vita.Status != state.TokenStatusActive { + panic(ErrSalusNoVita) + } + + // Get config for emergency duration + cfg := s.getConfigInternal(ic.DAO) + + // Get next emergency ID + cache := ic.DAO.GetRWCache(s.ID).(*SalusCache) + emergencyID := cache.emergencyCount + cache.emergencyCount++ + s.setEmergencyCounter(ic.DAO, cache.emergencyCount) + + // Create emergency access + emergency := &state.EmergencyAccess{ + ID: emergencyID, + VitaID: vita.TokenID, + Patient: patient, + Provider: provider, + Reason: reason, + GrantedAt: ic.Block.Index, + ExpiresAt: ic.Block.Index + cfg.EmergencyAccessDuration, + WasReviewed: false, + } + + s.putEmergency(ic.DAO, emergency) + + // Emit event + ic.AddNotification(s.Hash, EmergencyAccessGrantedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(emergencyID))), + stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), + stackitem.NewByteArray(provider.BytesBE()), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(emergencyID))) +} + +// reviewEmergencyAccess marks emergency access as reviewed (committee only). +func (s *Salus) reviewEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + emergencyID := toUint64(args[0]) + + // Committee only + if !s.checkCommittee(ic) { + panic(ErrSalusNotCommittee) + } + + emergency := s.getEmergencyInternal(ic.DAO, emergencyID) + if emergency == nil { + panic(ErrSalusEmergencyNotFound) + } + + // Mark as reviewed + emergency.WasReviewed = true + s.putEmergency(ic.DAO, emergency) + + // Emit event + ic.AddNotification(s.Hash, EmergencyAccessReviewedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(emergencyID))), + })) + + return stackitem.NewBool(true) +} + +// getEmergencyAccess returns emergency access details. +func (s *Salus) getEmergencyAccess(ic *interop.Context, args []stackitem.Item) stackitem.Item { + emergencyID := toUint64(args[0]) + + emergency := s.getEmergencyInternal(ic.DAO, emergencyID) + if emergency == nil { + return stackitem.Null{} + } + + item, _ := emergency.ToStackItem() + return item +} + +// getConfig returns the Salus configuration. +func (s *Salus) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cfg := s.getConfigInternal(ic.DAO) + item, _ := cfg.ToStackItem() + return item +} + +// getTotalAccounts returns the total number of healthcare accounts. +func (s *Salus) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*SalusCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount))) +} + +// getTotalRecords returns the total number of medical records. +func (s *Salus) getTotalRecords(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*SalusCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.recordCount))) +} + +// getTotalProviders returns the total number of healthcare providers. +func (s *Salus) getTotalProviders(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*SalusCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.providerCount))) +} + +// ===== Public Interface Methods for Cross-Contract Access ===== + +// GetAccountByOwner returns a healthcare account by owner address. +func (s *Salus) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.HealthcareAccount, error) { + vitaID, found := s.getVitaIDByOwner(d, owner) + if !found { + return nil, ErrSalusAccountNotFound + } + acc := s.getAccountInternal(d, vitaID) + if acc == nil { + return nil, ErrSalusAccountNotFound + } + return acc, nil +} + +// HasValidAuthorization checks if provider has valid authorization for patient. +func (s *Salus) HasValidAuthorization(d *dao.Simple, patient util.Uint160, provider util.Uint160, blockHeight uint32) bool { + vitaID, found := s.getVitaIDByOwner(d, patient) + if !found { + return false + } + + authID, exists := s.getActiveAuthID(d, vitaID, provider) + if !exists { + return false + } + + auth := s.getAuthInternal(d, authID) + if auth == nil { + return false + } + + return auth.IsValid(blockHeight) +} + +// Address returns the contract's script hash. +func (s *Salus) Address() util.Uint160 { + return s.Hash +} diff --git a/pkg/core/native/scire.go b/pkg/core/native/scire.go old mode 100644 new mode 100755 index 9f0a212..2abcf20 --- a/pkg/core/native/scire.go +++ b/pkg/core/native/scire.go @@ -1,1277 +1,1277 @@ -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/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" -) - -// Scire represents the universal education native contract. -type Scire struct { - interop.ContractMD - Tutus ITutus - Vita IVita - RoleRegistry IRoleRegistry - Lex ILex -} - -// ScireCache represents the cached state for Scire contract. -type ScireCache struct { - accountCount uint64 - certCount uint64 - enrollCount uint64 -} - -// Storage key prefixes for Scire. -const ( - scirePrefixAccount byte = 0x01 // vitaID -> EducationAccount - scirePrefixAccountByOwner byte = 0x02 // owner -> vitaID - scirePrefixCertification byte = 0x10 // certID -> Certification - scirePrefixCertByOwner byte = 0x11 // vitaID + certID -> exists - scirePrefixCertByInstitution byte = 0x12 // institution + certID -> exists - scirePrefixCertByType byte = 0x13 // certType hash + certID -> exists - scirePrefixEnrollment byte = 0x20 // enrollmentID -> Enrollment - scirePrefixEnrollByStudent byte = 0x21 // vitaID + enrollmentID -> exists - scirePrefixEnrollByProgram byte = 0x22 // programID hash + enrollmentID -> exists - scirePrefixActiveEnrollment byte = 0x23 // vitaID -> active enrollmentID - scirePrefixAccountCounter byte = 0xF0 // -> uint64 - scirePrefixCertCounter byte = 0xF1 // -> next certification ID - scirePrefixEnrollCounter byte = 0xF2 // -> next enrollment ID - scirePrefixConfig byte = 0xFF // -> ScireConfig -) - -// Event names for Scire. -const ( - AccountCreatedEvent = "AccountCreated" - CreditsAllocatedEvent = "CreditsAllocated" - EnrollmentCreatedEvent = "EnrollmentCreated" - EnrollmentCompletedEvent = "EnrollmentCompleted" - EnrollmentWithdrawnEvent = "EnrollmentWithdrawn" - EnrollmentTransferredEvent = "EnrollmentTransferred" - CertificationIssuedEvent = "CertificationIssued" - CertificationRevokedEvent = "CertificationRevoked" - CertificationRenewedEvent = "CertificationRenewed" -) - -// Role constants for educators. -const ( - RoleEducator uint64 = 20 // Can issue certifications and manage enrollments -) - -// Various errors for Scire. -var ( - ErrScireAccountNotFound = errors.New("education account not found") - ErrScireAccountExists = errors.New("education account already exists") - ErrScireAccountSuspended = errors.New("education account is suspended") - ErrScireAccountClosed = errors.New("education account is closed") - ErrScireNoVita = errors.New("owner must have an active Vita") - ErrScireInsufficientCredits = errors.New("insufficient education credits") - ErrScireInvalidCredits = errors.New("invalid credit amount") - ErrScireCertNotFound = errors.New("certification not found") - ErrScireCertExpired = errors.New("certification has expired") - ErrScireCertRevoked = errors.New("certification is revoked") - ErrScireEnrollNotFound = errors.New("enrollment not found") - ErrScireEnrollNotActive = errors.New("enrollment is not active") - ErrScireAlreadyEnrolled = errors.New("already enrolled in a program") - ErrScireNotEducator = errors.New("caller is not an authorized educator") - ErrScireNotCommittee = errors.New("invalid committee signature") - ErrScireInvalidOwner = errors.New("invalid owner address") - ErrScireInvalidInstitution = errors.New("invalid institution address") - ErrScireInvalidProgramID = errors.New("invalid program ID") - ErrScireInvalidCertType = errors.New("invalid certification type") - ErrScireInvalidName = errors.New("invalid certification name") - ErrScireEducationRestricted = errors.New("education right is restricted") - ErrScireNotStudent = errors.New("caller is not the student") - ErrScireNotInstitution = errors.New("caller is not the institution") - ErrScireExceedsMaxCredits = errors.New("exceeds maximum credits per program") -) - -var ( - _ interop.Contract = (*Scire)(nil) - _ dao.NativeContractCache = (*ScireCache)(nil) -) - -// Copy implements NativeContractCache interface. -func (c *ScireCache) Copy() dao.NativeContractCache { - return &ScireCache{ - accountCount: c.accountCount, - certCount: c.certCount, - enrollCount: c.enrollCount, - } -} - -// checkCommittee checks if the caller has committee authority. -func (s *Scire) checkCommittee(ic *interop.Context) bool { - if s.RoleRegistry != nil { - return s.RoleRegistry.CheckCommittee(ic) - } - return s.Tutus.CheckCommittee(ic) -} - -// checkEducator checks if the caller has educator authority. -func (s *Scire) checkEducator(ic *interop.Context) bool { - caller := ic.VM.GetCallingScriptHash() - if s.RoleRegistry != nil { - if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleEducator, ic.Block.Index) { - return true - } - } - // Committee members can also act as educators - return s.checkCommittee(ic) -} - -// checkEducationRight checks if subject has education rights via Lex. -func (s *Scire) checkEducationRight(ic *interop.Context, subject util.Uint160) bool { - if s.Lex == nil { - return true // Allow if Lex not available - } - return s.Lex.HasRightInternal(ic.DAO, subject, state.RightEducation, ic.Block.Index) -} - -// newScire creates a new Scire native contract. -func newScire() *Scire { - s := &Scire{ - ContractMD: *interop.NewContractMD(nativenames.Scire, nativeids.Scire), - } - defer s.BuildHFSpecificMD(s.ActiveIn()) - - // ===== Account Management ===== - - // createAccount - Create education account for a Vita holder - desc := NewDescriptor("createAccount", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md := NewMethodAndPrice(s.createAccount, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getAccount - Get education account by owner - desc = NewDescriptor("getAccount", smartcontract.ArrayType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getAccountByVitaID - Get account by Vita ID - desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, - manifest.NewParameter("vitaID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // allocateCredits - Allocate learning credits (committee only) - desc = NewDescriptor("allocateCredits", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getCredits - Get available credits - desc = NewDescriptor("getCredits", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Enrollment Management ===== - - // enroll - Enroll in an education program - desc = NewDescriptor("enroll", smartcontract.IntegerType, - manifest.NewParameter("student", smartcontract.Hash160Type), - manifest.NewParameter("programID", smartcontract.StringType), - manifest.NewParameter("institution", smartcontract.Hash160Type), - manifest.NewParameter("credits", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.enroll, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // completeEnrollment - Mark enrollment as completed (institution only) - desc = NewDescriptor("completeEnrollment", smartcontract.BoolType, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType), - manifest.NewParameter("contentHash", smartcontract.Hash256Type)) - md = NewMethodAndPrice(s.completeEnrollment, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // withdrawEnrollment - Withdraw from program - desc = NewDescriptor("withdrawEnrollment", smartcontract.BoolType, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.withdrawEnrollment, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getEnrollment - Get enrollment details - desc = NewDescriptor("getEnrollment", smartcontract.ArrayType, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getEnrollment, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getActiveEnrollment - Get student's active enrollment - desc = NewDescriptor("getActiveEnrollment", smartcontract.ArrayType, - manifest.NewParameter("student", smartcontract.Hash160Type)) - md = NewMethodAndPrice(s.getActiveEnrollment, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Certification Management ===== - - // issueCertification - Issue a certification (educator only) - desc = NewDescriptor("issueCertification", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("certType", smartcontract.StringType), - manifest.NewParameter("name", smartcontract.StringType), - manifest.NewParameter("contentHash", smartcontract.Hash256Type), - manifest.NewParameter("expiresAt", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.issueCertification, 1<<17, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // revokeCertification - Revoke a certification (institution only) - desc = NewDescriptor("revokeCertification", smartcontract.BoolType, - manifest.NewParameter("certID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(s.revokeCertification, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // renewCertification - Extend certification expiry - desc = NewDescriptor("renewCertification", smartcontract.BoolType, - manifest.NewParameter("certID", smartcontract.IntegerType), - manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.renewCertification, 1<<16, callflag.States|callflag.AllowNotify) - s.AddMethod(md, desc) - - // getCertification - Get certification details - desc = NewDescriptor("getCertification", smartcontract.ArrayType, - manifest.NewParameter("certID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.getCertification, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // verifyCertification - Check if certification is valid - desc = NewDescriptor("verifyCertification", smartcontract.BoolType, - manifest.NewParameter("certID", smartcontract.IntegerType)) - md = NewMethodAndPrice(s.verifyCertification, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // hasCertification - Check if owner has specific cert type - desc = NewDescriptor("hasCertification", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("certType", smartcontract.StringType)) - md = NewMethodAndPrice(s.hasCertification, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Query Methods ===== - - // getConfig - Get Scire configuration - desc = NewDescriptor("getConfig", smartcontract.ArrayType) - md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalAccounts - Get total education accounts - desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalCertifications - Get total certifications issued - desc = NewDescriptor("getTotalCertifications", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalCertifications, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // getTotalEnrollments - Get total enrollments - desc = NewDescriptor("getTotalEnrollments", smartcontract.IntegerType) - md = NewMethodAndPrice(s.getTotalEnrollments, 1<<15, callflag.ReadStates) - s.AddMethod(md, desc) - - // ===== Events ===== - - // AccountCreated event - eDesc := NewEventDescriptor(AccountCreatedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("owner", smartcontract.Hash160Type)) - s.AddEvent(NewEvent(eDesc)) - - // CreditsAllocated event - eDesc = NewEventDescriptor(CreditsAllocatedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("total", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - // EnrollmentCreated event - eDesc = NewEventDescriptor(EnrollmentCreatedEvent, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("programID", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // EnrollmentCompleted event - eDesc = NewEventDescriptor(EnrollmentCompletedEvent, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - // EnrollmentWithdrawn event - eDesc = NewEventDescriptor(EnrollmentWithdrawnEvent, - manifest.NewParameter("enrollmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // CertificationIssued event - eDesc = NewEventDescriptor(CertificationIssuedEvent, - manifest.NewParameter("certID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("certType", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // CertificationRevoked event - eDesc = NewEventDescriptor(CertificationRevokedEvent, - manifest.NewParameter("certID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - s.AddEvent(NewEvent(eDesc)) - - // CertificationRenewed event - eDesc = NewEventDescriptor(CertificationRenewedEvent, - manifest.NewParameter("certID", smartcontract.IntegerType), - manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) - s.AddEvent(NewEvent(eDesc)) - - return s -} - -// Metadata returns contract metadata. -func (s *Scire) Metadata() *interop.ContractMD { - return &s.ContractMD -} - -// Initialize initializes the Scire contract. -func (s *Scire) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != s.ActiveIn() { - return nil - } - - // Initialize counters - s.setAccountCounter(ic.DAO, 0) - s.setCertCounter(ic.DAO, 0) - s.setEnrollCounter(ic.DAO, 0) - - // Initialize config with defaults - cfg := &state.ScireConfig{ - AnnualCreditAllocation: 1000, // 1000 credits per year - MaxCreditsPerProgram: 500, // Max 500 credits per program - CertificationFee: 0, // Free certification - MinEnrollmentDuration: 86400, // ~1 day in blocks (1-second blocks) - } - s.setConfig(ic.DAO, cfg) - - // Initialize cache - cache := &ScireCache{ - accountCount: 0, - certCount: 0, - enrollCount: 0, - } - ic.DAO.SetCache(s.ID, cache) - - return nil -} - -// InitializeCache initializes the cache from storage. -func (s *Scire) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - cache := &ScireCache{ - accountCount: s.getAccountCounter(d), - certCount: s.getCertCounter(d), - enrollCount: s.getEnrollCounter(d), - } - d.SetCache(s.ID, cache) - return nil -} - -// OnPersist is called before block is committed. -func (s *Scire) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist is called after block is committed. -func (s *Scire) PostPersist(ic *interop.Context) error { - return nil -} - -// ActiveIn returns the hardfork at which this contract is activated. -func (s *Scire) ActiveIn() *config.Hardfork { - return nil // Always active -} - -// ===== Storage Helpers ===== - -func (s *Scire) makeAccountKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = scirePrefixAccount - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -func (s *Scire) makeAccountByOwnerKey(owner util.Uint160) []byte { - key := make([]byte, 21) - key[0] = scirePrefixAccountByOwner - copy(key[1:], owner.BytesBE()) - return key -} - -func (s *Scire) makeCertificationKey(certID uint64) []byte { - key := make([]byte, 9) - key[0] = scirePrefixCertification - binary.BigEndian.PutUint64(key[1:], certID) - return key -} - -func (s *Scire) makeCertByOwnerKey(vitaID, certID uint64) []byte { - key := make([]byte, 17) - key[0] = scirePrefixCertByOwner - binary.BigEndian.PutUint64(key[1:9], vitaID) - binary.BigEndian.PutUint64(key[9:], certID) - return key -} - -func (s *Scire) makeEnrollmentKey(enrollID uint64) []byte { - key := make([]byte, 9) - key[0] = scirePrefixEnrollment - binary.BigEndian.PutUint64(key[1:], enrollID) - return key -} - -func (s *Scire) makeActiveEnrollmentKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = scirePrefixActiveEnrollment - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -// Counter getters/setters -func (s *Scire) getAccountCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{scirePrefixAccountCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Scire) setAccountCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{scirePrefixAccountCounter}, buf) -} - -func (s *Scire) getCertCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{scirePrefixCertCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Scire) setCertCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{scirePrefixCertCounter}, buf) -} - -func (s *Scire) getEnrollCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(s.ID, []byte{scirePrefixEnrollCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Scire) setEnrollCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(s.ID, []byte{scirePrefixEnrollCounter}, buf) -} - -// Config getter/setter -func (s *Scire) getConfigInternal(d *dao.Simple) *state.ScireConfig { - si := d.GetStorageItem(s.ID, []byte{scirePrefixConfig}) - if si == nil { - return &state.ScireConfig{ - AnnualCreditAllocation: 1000, - MaxCreditsPerProgram: 500, - CertificationFee: 0, - MinEnrollmentDuration: 86400, - } - } - cfg := new(state.ScireConfig) - item, _ := stackitem.Deserialize(si) - cfg.FromStackItem(item) - return cfg -} - -func (s *Scire) setConfig(d *dao.Simple, cfg *state.ScireConfig) { - item, _ := cfg.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, []byte{scirePrefixConfig}, data) -} - -// Account storage -func (s *Scire) getAccountInternal(d *dao.Simple, vitaID uint64) *state.EducationAccount { - si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID)) - if si == nil { - return nil - } - acc := new(state.EducationAccount) - item, _ := stackitem.Deserialize(si) - acc.FromStackItem(item) - return acc -} - -func (s *Scire) putAccount(d *dao.Simple, acc *state.EducationAccount) { - item, _ := acc.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data) -} - -func (s *Scire) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { - si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (s *Scire) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, vitaID) - d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf) -} - -// Enrollment storage -func (s *Scire) getEnrollmentInternal(d *dao.Simple, enrollID uint64) *state.Enrollment { - si := d.GetStorageItem(s.ID, s.makeEnrollmentKey(enrollID)) - if si == nil { - return nil - } - enroll := new(state.Enrollment) - item, _ := stackitem.Deserialize(si) - enroll.FromStackItem(item) - return enroll -} - -func (s *Scire) putEnrollment(d *dao.Simple, enroll *state.Enrollment) { - item, _ := enroll.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeEnrollmentKey(enroll.ID), data) -} - -func (s *Scire) getActiveEnrollmentID(d *dao.Simple, vitaID uint64) uint64 { - si := d.GetStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (s *Scire) setActiveEnrollmentID(d *dao.Simple, vitaID, enrollID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, enrollID) - d.PutStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID), buf) -} - -func (s *Scire) clearActiveEnrollment(d *dao.Simple, vitaID uint64) { - d.DeleteStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) -} - -// Certification storage -func (s *Scire) getCertificationInternal(d *dao.Simple, certID uint64) *state.Certification { - si := d.GetStorageItem(s.ID, s.makeCertificationKey(certID)) - if si == nil { - return nil - } - cert := new(state.Certification) - item, _ := stackitem.Deserialize(si) - cert.FromStackItem(item) - return cert -} - -func (s *Scire) putCertification(d *dao.Simple, cert *state.Certification) { - item, _ := cert.ToStackItem() - data, _ := stackitem.Serialize(item) - d.PutStorageItem(s.ID, s.makeCertificationKey(cert.ID), data) -} - -func (s *Scire) setCertByOwner(d *dao.Simple, vitaID, certID uint64) { - d.PutStorageItem(s.ID, s.makeCertByOwnerKey(vitaID, certID), []byte{1}) -} - -// ===== Contract Methods ===== - -// createAccount creates an education account for a Vita holder. -func (s *Scire) createAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - // Check owner has active Vita - if s.Vita == nil { - panic(ErrScireNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil { - panic(ErrScireNoVita) - } - if vita.Status != state.TokenStatusActive { - panic(ErrScireNoVita) - } - - // Check if account already exists - existing := s.getAccountInternal(ic.DAO, vita.TokenID) - if existing != nil { - panic(ErrScireAccountExists) - } - - // Check education rights - if !s.checkEducationRight(ic, owner) { - // Log but allow (EnforcementLogging) - // In the future, we could emit an event here - } - - // Get cache and increment counter - cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) - accountNum := cache.accountCount - cache.accountCount++ - s.setAccountCounter(ic.DAO, cache.accountCount) - - // Create account - acc := &state.EducationAccount{ - VitaID: vita.TokenID, - Owner: owner, - TotalCredits: 0, - UsedCredits: 0, - AvailableCredits: 0, - Status: state.EducationAccountActive, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - - // Store account - s.putAccount(ic.DAO, acc) - s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID) - - // Emit event - ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), - stackitem.NewByteArray(owner.BytesBE()), - })) - - _ = accountNum // suppress unused warning - return stackitem.NewBool(true) -} - -// getAccount returns education account by owner. -func (s *Scire) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.Null{} - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.Null{} - } - - item, _ := acc.ToStackItem() - return item -} - -// getAccountByVitaID returns education account by Vita ID. -func (s *Scire) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { - vitaID := toUint64(args[0]) - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.Null{} - } - - item, _ := acc.ToStackItem() - return item -} - -// allocateCredits allocates learning credits to an account (committee only). -func (s *Scire) allocateCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - amount := toUint64(args[1]) - // reason := toString(args[2]) // for logging - - // Committee only - if !s.checkCommittee(ic) { - panic(ErrScireNotCommittee) - } - - if amount == 0 { - panic(ErrScireInvalidCredits) - } - - // Get or create account - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - // Auto-create account if Vita exists - if s.Vita == nil { - panic(ErrScireNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil || vita.Status != state.TokenStatusActive { - panic(ErrScireNoVita) - } - vitaID = vita.TokenID - - // Create account - cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) - cache.accountCount++ - s.setAccountCounter(ic.DAO, cache.accountCount) - - acc := &state.EducationAccount{ - VitaID: vitaID, - Owner: owner, - TotalCredits: amount, - UsedCredits: 0, - AvailableCredits: amount, - Status: state.EducationAccountActive, - CreatedAt: ic.Block.Index, - UpdatedAt: ic.Block.Index, - } - s.putAccount(ic.DAO, acc) - s.setOwnerToVitaID(ic.DAO, owner, vitaID) - - // Emit events - ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewByteArray(owner.BytesBE()), - })) - ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - })) - - return stackitem.NewBool(true) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - panic(ErrScireAccountNotFound) - } - if acc.Status != state.EducationAccountActive { - panic(ErrScireAccountSuspended) - } - - // Add credits - acc.TotalCredits += amount - acc.AvailableCredits += amount - acc.UpdatedAt = ic.Block.Index - - s.putAccount(ic.DAO, acc) - - // Emit event - ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewBigInteger(big.NewInt(int64(amount))), - stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))), - })) - - return stackitem.NewBool(true) -} - -// getCredits returns available credits for an owner. -func (s *Scire) getCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - return stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))) -} - -// enroll enrolls a student in an education program. -func (s *Scire) enroll(ic *interop.Context, args []stackitem.Item) stackitem.Item { - student := toUint160(args[0]) - programID := toString(args[1]) - institution := toUint160(args[2]) - credits := toUint64(args[3]) - - // Validate inputs - if len(programID) == 0 || len(programID) > 128 { - panic(ErrScireInvalidProgramID) - } - if credits == 0 { - panic(ErrScireInvalidCredits) - } - - // Check max credits - cfg := s.getConfigInternal(ic.DAO) - if credits > cfg.MaxCreditsPerProgram { - panic(ErrScireExceedsMaxCredits) - } - - // Get student's account - vitaID, found := s.getVitaIDByOwner(ic.DAO, student) - if !found { - panic(ErrScireAccountNotFound) - } - - acc := s.getAccountInternal(ic.DAO, vitaID) - if acc == nil { - panic(ErrScireAccountNotFound) - } - if acc.Status != state.EducationAccountActive { - panic(ErrScireAccountSuspended) - } - - // Check sufficient credits - if acc.AvailableCredits < credits { - panic(ErrScireInsufficientCredits) - } - - // Check not already enrolled - activeEnrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) - if activeEnrollID != 0 { - panic(ErrScireAlreadyEnrolled) - } - - // Check education rights - if !s.checkEducationRight(ic, student) { - // Log but allow - } - - // Get next enrollment ID - cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) - enrollID := cache.enrollCount - cache.enrollCount++ - s.setEnrollCounter(ic.DAO, cache.enrollCount) - - // Deduct credits - acc.UsedCredits += credits - acc.AvailableCredits -= credits - acc.UpdatedAt = ic.Block.Index - s.putAccount(ic.DAO, acc) - - // Create enrollment - enroll := &state.Enrollment{ - ID: enrollID, - VitaID: vitaID, - Student: student, - ProgramID: programID, - Institution: institution, - CreditsAllocated: credits, - StartedAt: ic.Block.Index, - CompletedAt: 0, - Status: state.EnrollmentActive, - } - - s.putEnrollment(ic.DAO, enroll) - s.setActiveEnrollmentID(ic.DAO, vitaID, enrollID) - - // Emit event - ic.AddNotification(s.Hash, EnrollmentCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(enrollID))), - stackitem.NewBigInteger(big.NewInt(int64(vitaID))), - stackitem.NewByteArray([]byte(programID)), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(enrollID))) -} - -// completeEnrollment marks an enrollment as completed (institution/educator only). -func (s *Scire) completeEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - enrollID := toUint64(args[0]) - // contentHash := toUint256(args[1]) // for proof storage - - enroll := s.getEnrollmentInternal(ic.DAO, enrollID) - if enroll == nil { - panic(ErrScireEnrollNotFound) - } - if enroll.Status != state.EnrollmentActive { - panic(ErrScireEnrollNotActive) - } - - // Check caller is institution or educator - caller := ic.VM.GetCallingScriptHash() - if caller != enroll.Institution && !s.checkEducator(ic) { - panic(ErrScireNotInstitution) - } - - // Update enrollment - enroll.Status = state.EnrollmentCompleted - enroll.CompletedAt = ic.Block.Index - s.putEnrollment(ic.DAO, enroll) - - // Clear active enrollment - s.clearActiveEnrollment(ic.DAO, enroll.VitaID) - - // Emit event - ic.AddNotification(s.Hash, EnrollmentCompletedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(enrollID))), - stackitem.NewBigInteger(big.NewInt(int64(enroll.VitaID))), - })) - - return stackitem.NewBool(true) -} - -// withdrawEnrollment withdraws from an enrollment. -func (s *Scire) withdrawEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - enrollID := toUint64(args[0]) - reason := toString(args[1]) - - enroll := s.getEnrollmentInternal(ic.DAO, enrollID) - if enroll == nil { - panic(ErrScireEnrollNotFound) - } - if enroll.Status != state.EnrollmentActive { - panic(ErrScireEnrollNotActive) - } - - // Check caller is student or institution - caller := ic.VM.GetCallingScriptHash() - if caller != enroll.Student && caller != enroll.Institution && !s.checkEducator(ic) { - panic(ErrScireNotStudent) - } - - // Update enrollment - enroll.Status = state.EnrollmentWithdrawn - enroll.CompletedAt = ic.Block.Index - s.putEnrollment(ic.DAO, enroll) - - // Clear active enrollment - s.clearActiveEnrollment(ic.DAO, enroll.VitaID) - - // Partial refund: return 50% of credits - acc := s.getAccountInternal(ic.DAO, enroll.VitaID) - if acc != nil { - refund := enroll.CreditsAllocated / 2 - acc.AvailableCredits += refund - acc.UsedCredits -= refund - acc.UpdatedAt = ic.Block.Index - s.putAccount(ic.DAO, acc) - } - - // Emit event - ic.AddNotification(s.Hash, EnrollmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(enrollID))), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// getEnrollment returns enrollment details. -func (s *Scire) getEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - enrollID := toUint64(args[0]) - - enroll := s.getEnrollmentInternal(ic.DAO, enrollID) - if enroll == nil { - return stackitem.Null{} - } - - item, _ := enroll.ToStackItem() - return item -} - -// getActiveEnrollment returns a student's active enrollment. -func (s *Scire) getActiveEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - student := toUint160(args[0]) - - vitaID, found := s.getVitaIDByOwner(ic.DAO, student) - if !found { - return stackitem.Null{} - } - - enrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) - if enrollID == 0 { - return stackitem.Null{} - } - - enroll := s.getEnrollmentInternal(ic.DAO, enrollID) - if enroll == nil { - return stackitem.Null{} - } - - item, _ := enroll.ToStackItem() - return item -} - -// issueCertification issues a certification (educator only). -func (s *Scire) issueCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - certType := toString(args[1]) - name := toString(args[2]) - contentHashBytes := toBytes(args[3]) - expiresAt := toUint32(args[4]) - - // Convert bytes to Uint256 - var contentHash util.Uint256 - if len(contentHashBytes) == 32 { - copy(contentHash[:], contentHashBytes) - } - - // Validate inputs - if len(certType) == 0 || len(certType) > 64 { - panic(ErrScireInvalidCertType) - } - if len(name) == 0 || len(name) > 128 { - panic(ErrScireInvalidName) - } - - // Check educator authority - if !s.checkEducator(ic) { - panic(ErrScireNotEducator) - } - - // Get owner's Vita - if s.Vita == nil { - panic(ErrScireNoVita) - } - vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil || vita.Status != state.TokenStatusActive { - panic(ErrScireNoVita) - } - - // Get issuing institution - institution := ic.VM.GetCallingScriptHash() - - // Get next cert ID - cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) - certID := cache.certCount - cache.certCount++ - s.setCertCounter(ic.DAO, cache.certCount) - - // Create certification - cert := &state.Certification{ - ID: certID, - VitaID: vita.TokenID, - Owner: owner, - CertType: certType, - Name: name, - Institution: institution, - ContentHash: contentHash, - IssuedAt: ic.Block.Index, - ExpiresAt: expiresAt, - Status: state.CertificationActive, - } - - s.putCertification(ic.DAO, cert) - s.setCertByOwner(ic.DAO, vita.TokenID, certID) - - // Emit event - ic.AddNotification(s.Hash, CertificationIssuedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(certID))), - stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), - stackitem.NewByteArray([]byte(certType)), - })) - - return stackitem.NewBigInteger(big.NewInt(int64(certID))) -} - -// revokeCertification revokes a certification (institution only). -func (s *Scire) revokeCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - certID := toUint64(args[0]) - reason := toString(args[1]) - - cert := s.getCertificationInternal(ic.DAO, certID) - if cert == nil { - panic(ErrScireCertNotFound) - } - if cert.Status == state.CertificationRevoked { - panic(ErrScireCertRevoked) - } - - // Check caller is issuing institution or educator - caller := ic.VM.GetCallingScriptHash() - if caller != cert.Institution && !s.checkEducator(ic) { - panic(ErrScireNotInstitution) - } - - // Revoke - cert.Status = state.CertificationRevoked - s.putCertification(ic.DAO, cert) - - // Emit event - ic.AddNotification(s.Hash, CertificationRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(certID))), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// renewCertification extends a certification's expiry. -func (s *Scire) renewCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - certID := toUint64(args[0]) - newExpiresAt := toUint32(args[1]) - - cert := s.getCertificationInternal(ic.DAO, certID) - if cert == nil { - panic(ErrScireCertNotFound) - } - if cert.Status == state.CertificationRevoked { - panic(ErrScireCertRevoked) - } - - // Check caller is issuing institution or educator - caller := ic.VM.GetCallingScriptHash() - if caller != cert.Institution && !s.checkEducator(ic) { - panic(ErrScireNotInstitution) - } - - // Update expiry - cert.ExpiresAt = newExpiresAt - cert.Status = state.CertificationActive // Reactivate if was expired - s.putCertification(ic.DAO, cert) - - // Emit event - ic.AddNotification(s.Hash, CertificationRenewedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(certID))), - stackitem.NewBigInteger(big.NewInt(int64(newExpiresAt))), - })) - - return stackitem.NewBool(true) -} - -// getCertification returns certification details. -func (s *Scire) getCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - certID := toUint64(args[0]) - - cert := s.getCertificationInternal(ic.DAO, certID) - if cert == nil { - return stackitem.Null{} - } - - item, _ := cert.ToStackItem() - return item -} - -// verifyCertification checks if a certification is currently valid. -func (s *Scire) verifyCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - certID := toUint64(args[0]) - - cert := s.getCertificationInternal(ic.DAO, certID) - if cert == nil { - return stackitem.NewBool(false) - } - - return stackitem.NewBool(cert.IsValid(ic.Block.Index)) -} - -// hasCertification checks if an owner has a specific certification type. -func (s *Scire) hasCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - certType := toString(args[1]) - - vitaID, exists := s.getVitaIDByOwner(ic.DAO, owner) - if !exists { - return stackitem.NewBool(false) - } - - // Scan certifications for this owner - prefix := make([]byte, 9) - prefix[0] = scirePrefixCertByOwner - binary.BigEndian.PutUint64(prefix[1:], vitaID) - - var hasCert bool - ic.DAO.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 8 { - certID := binary.BigEndian.Uint64(k[len(k)-8:]) - cert := s.getCertificationInternal(ic.DAO, certID) - if cert != nil && cert.CertType == certType && cert.IsValid(ic.Block.Index) { - hasCert = true - return false // Stop iteration - } - } - return true - }) - - return stackitem.NewBool(hasCert) -} - -// getConfig returns the Scire configuration. -func (s *Scire) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cfg := s.getConfigInternal(ic.DAO) - item, _ := cfg.ToStackItem() - return item -} - -// getTotalAccounts returns the total number of education accounts. -func (s *Scire) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*ScireCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount))) -} - -// getTotalCertifications returns the total number of certifications. -func (s *Scire) getTotalCertifications(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*ScireCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.certCount))) -} - -// getTotalEnrollments returns the total number of enrollments. -func (s *Scire) getTotalEnrollments(ic *interop.Context, args []stackitem.Item) stackitem.Item { - cache := ic.DAO.GetROCache(s.ID).(*ScireCache) - return stackitem.NewBigInteger(big.NewInt(int64(cache.enrollCount))) -} - -// ===== Public Interface Methods for Cross-Contract Access ===== - -// GetAccountByOwner returns an education account by owner address. -func (s *Scire) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.EducationAccount, error) { - vitaID, found := s.getVitaIDByOwner(d, owner) - if !found { - return nil, ErrScireAccountNotFound - } - acc := s.getAccountInternal(d, vitaID) - if acc == nil { - return nil, ErrScireAccountNotFound - } - return acc, nil -} - -// HasValidCertification checks if owner has a valid certification of the given type. -func (s *Scire) HasValidCertification(d *dao.Simple, owner util.Uint160, certType string, blockHeight uint32) bool { - vitaID, exists := s.getVitaIDByOwner(d, owner) - if !exists { - return false - } - - prefix := make([]byte, 9) - prefix[0] = scirePrefixCertByOwner - binary.BigEndian.PutUint64(prefix[1:], vitaID) - - var hasCert bool - d.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - if len(k) >= 8 { - certID := binary.BigEndian.Uint64(k[len(k)-8:]) - cert := s.getCertificationInternal(d, certID) - if cert != nil && cert.CertType == certType { - if cert.Status == state.CertificationActive { - if cert.ExpiresAt == 0 || cert.ExpiresAt > blockHeight { - hasCert = true - return false - } - } - } - } - return true - }) - - return hasCert -} - -// Address returns the contract's script hash. -func (s *Scire) Address() util.Uint160 { - return s.Hash -} +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/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" +) + +// Scire represents the universal education native contract. +type Scire struct { + interop.ContractMD + Tutus ITutus + Vita IVita + RoleRegistry IRoleRegistry + Lex ILex +} + +// ScireCache represents the cached state for Scire contract. +type ScireCache struct { + accountCount uint64 + certCount uint64 + enrollCount uint64 +} + +// Storage key prefixes for Scire. +const ( + scirePrefixAccount byte = 0x01 // vitaID -> EducationAccount + scirePrefixAccountByOwner byte = 0x02 // owner -> vitaID + scirePrefixCertification byte = 0x10 // certID -> Certification + scirePrefixCertByOwner byte = 0x11 // vitaID + certID -> exists + scirePrefixCertByInstitution byte = 0x12 // institution + certID -> exists + scirePrefixCertByType byte = 0x13 // certType hash + certID -> exists + scirePrefixEnrollment byte = 0x20 // enrollmentID -> Enrollment + scirePrefixEnrollByStudent byte = 0x21 // vitaID + enrollmentID -> exists + scirePrefixEnrollByProgram byte = 0x22 // programID hash + enrollmentID -> exists + scirePrefixActiveEnrollment byte = 0x23 // vitaID -> active enrollmentID + scirePrefixAccountCounter byte = 0xF0 // -> uint64 + scirePrefixCertCounter byte = 0xF1 // -> next certification ID + scirePrefixEnrollCounter byte = 0xF2 // -> next enrollment ID + scirePrefixConfig byte = 0xFF // -> ScireConfig +) + +// Event names for Scire. +const ( + AccountCreatedEvent = "AccountCreated" + CreditsAllocatedEvent = "CreditsAllocated" + EnrollmentCreatedEvent = "EnrollmentCreated" + EnrollmentCompletedEvent = "EnrollmentCompleted" + EnrollmentWithdrawnEvent = "EnrollmentWithdrawn" + EnrollmentTransferredEvent = "EnrollmentTransferred" + CertificationIssuedEvent = "CertificationIssued" + CertificationRevokedEvent = "CertificationRevoked" + CertificationRenewedEvent = "CertificationRenewed" +) + +// Role constants for educators. +const ( + RoleEducator uint64 = 20 // Can issue certifications and manage enrollments +) + +// Various errors for Scire. +var ( + ErrScireAccountNotFound = errors.New("education account not found") + ErrScireAccountExists = errors.New("education account already exists") + ErrScireAccountSuspended = errors.New("education account is suspended") + ErrScireAccountClosed = errors.New("education account is closed") + ErrScireNoVita = errors.New("owner must have an active Vita") + ErrScireInsufficientCredits = errors.New("insufficient education credits") + ErrScireInvalidCredits = errors.New("invalid credit amount") + ErrScireCertNotFound = errors.New("certification not found") + ErrScireCertExpired = errors.New("certification has expired") + ErrScireCertRevoked = errors.New("certification is revoked") + ErrScireEnrollNotFound = errors.New("enrollment not found") + ErrScireEnrollNotActive = errors.New("enrollment is not active") + ErrScireAlreadyEnrolled = errors.New("already enrolled in a program") + ErrScireNotEducator = errors.New("caller is not an authorized educator") + ErrScireNotCommittee = errors.New("invalid committee signature") + ErrScireInvalidOwner = errors.New("invalid owner address") + ErrScireInvalidInstitution = errors.New("invalid institution address") + ErrScireInvalidProgramID = errors.New("invalid program ID") + ErrScireInvalidCertType = errors.New("invalid certification type") + ErrScireInvalidName = errors.New("invalid certification name") + ErrScireEducationRestricted = errors.New("education right is restricted") + ErrScireNotStudent = errors.New("caller is not the student") + ErrScireNotInstitution = errors.New("caller is not the institution") + ErrScireExceedsMaxCredits = errors.New("exceeds maximum credits per program") +) + +var ( + _ interop.Contract = (*Scire)(nil) + _ dao.NativeContractCache = (*ScireCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *ScireCache) Copy() dao.NativeContractCache { + return &ScireCache{ + accountCount: c.accountCount, + certCount: c.certCount, + enrollCount: c.enrollCount, + } +} + +// checkCommittee checks if the caller has committee authority. +func (s *Scire) checkCommittee(ic *interop.Context) bool { + if s.RoleRegistry != nil { + return s.RoleRegistry.CheckCommittee(ic) + } + return s.Tutus.CheckCommittee(ic) +} + +// checkEducator checks if the caller has educator authority. +func (s *Scire) checkEducator(ic *interop.Context) bool { + caller := ic.VM.GetCallingScriptHash() + if s.RoleRegistry != nil { + if s.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleEducator, ic.Block.Index) { + return true + } + } + // Committee members can also act as educators + return s.checkCommittee(ic) +} + +// checkEducationRight checks if subject has education rights via Lex. +func (s *Scire) checkEducationRight(ic *interop.Context, subject util.Uint160) bool { + if s.Lex == nil { + return true // Allow if Lex not available + } + return s.Lex.HasRightInternal(ic.DAO, subject, state.RightEducation, ic.Block.Index) +} + +// newScire creates a new Scire native contract. +func newScire() *Scire { + s := &Scire{ + ContractMD: *interop.NewContractMD(nativenames.Scire, nativeids.Scire), + } + defer s.BuildHFSpecificMD(s.ActiveIn()) + + // ===== Account Management ===== + + // createAccount - Create education account for a Vita holder + desc := NewDescriptor("createAccount", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md := NewMethodAndPrice(s.createAccount, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getAccount - Get education account by owner + desc = NewDescriptor("getAccount", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getAccount, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getAccountByVitaID - Get account by Vita ID + desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, + manifest.NewParameter("vitaID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getAccountByVitaID, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // allocateCredits - Allocate learning credits (committee only) + desc = NewDescriptor("allocateCredits", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.allocateCredits, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getCredits - Get available credits + desc = NewDescriptor("getCredits", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getCredits, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Enrollment Management ===== + + // enroll - Enroll in an education program + desc = NewDescriptor("enroll", smartcontract.IntegerType, + manifest.NewParameter("student", smartcontract.Hash160Type), + manifest.NewParameter("programID", smartcontract.StringType), + manifest.NewParameter("institution", smartcontract.Hash160Type), + manifest.NewParameter("credits", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.enroll, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // completeEnrollment - Mark enrollment as completed (institution only) + desc = NewDescriptor("completeEnrollment", smartcontract.BoolType, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType), + manifest.NewParameter("contentHash", smartcontract.Hash256Type)) + md = NewMethodAndPrice(s.completeEnrollment, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // withdrawEnrollment - Withdraw from program + desc = NewDescriptor("withdrawEnrollment", smartcontract.BoolType, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.withdrawEnrollment, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getEnrollment - Get enrollment details + desc = NewDescriptor("getEnrollment", smartcontract.ArrayType, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getEnrollment, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getActiveEnrollment - Get student's active enrollment + desc = NewDescriptor("getActiveEnrollment", smartcontract.ArrayType, + manifest.NewParameter("student", smartcontract.Hash160Type)) + md = NewMethodAndPrice(s.getActiveEnrollment, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Certification Management ===== + + // issueCertification - Issue a certification (educator only) + desc = NewDescriptor("issueCertification", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("certType", smartcontract.StringType), + manifest.NewParameter("name", smartcontract.StringType), + manifest.NewParameter("contentHash", smartcontract.Hash256Type), + manifest.NewParameter("expiresAt", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.issueCertification, 1<<17, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // revokeCertification - Revoke a certification (institution only) + desc = NewDescriptor("revokeCertification", smartcontract.BoolType, + manifest.NewParameter("certID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(s.revokeCertification, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // renewCertification - Extend certification expiry + desc = NewDescriptor("renewCertification", smartcontract.BoolType, + manifest.NewParameter("certID", smartcontract.IntegerType), + manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.renewCertification, 1<<16, callflag.States|callflag.AllowNotify) + s.AddMethod(md, desc) + + // getCertification - Get certification details + desc = NewDescriptor("getCertification", smartcontract.ArrayType, + manifest.NewParameter("certID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.getCertification, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // verifyCertification - Check if certification is valid + desc = NewDescriptor("verifyCertification", smartcontract.BoolType, + manifest.NewParameter("certID", smartcontract.IntegerType)) + md = NewMethodAndPrice(s.verifyCertification, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // hasCertification - Check if owner has specific cert type + desc = NewDescriptor("hasCertification", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("certType", smartcontract.StringType)) + md = NewMethodAndPrice(s.hasCertification, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Query Methods ===== + + // getConfig - Get Scire configuration + desc = NewDescriptor("getConfig", smartcontract.ArrayType) + md = NewMethodAndPrice(s.getConfig, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalAccounts - Get total education accounts + desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalAccounts, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalCertifications - Get total certifications issued + desc = NewDescriptor("getTotalCertifications", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalCertifications, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // getTotalEnrollments - Get total enrollments + desc = NewDescriptor("getTotalEnrollments", smartcontract.IntegerType) + md = NewMethodAndPrice(s.getTotalEnrollments, 1<<15, callflag.ReadStates) + s.AddMethod(md, desc) + + // ===== Events ===== + + // AccountCreated event + eDesc := NewEventDescriptor(AccountCreatedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("owner", smartcontract.Hash160Type)) + s.AddEvent(NewEvent(eDesc)) + + // CreditsAllocated event + eDesc = NewEventDescriptor(CreditsAllocatedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("total", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + // EnrollmentCreated event + eDesc = NewEventDescriptor(EnrollmentCreatedEvent, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("programID", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // EnrollmentCompleted event + eDesc = NewEventDescriptor(EnrollmentCompletedEvent, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + // EnrollmentWithdrawn event + eDesc = NewEventDescriptor(EnrollmentWithdrawnEvent, + manifest.NewParameter("enrollmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // CertificationIssued event + eDesc = NewEventDescriptor(CertificationIssuedEvent, + manifest.NewParameter("certID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("certType", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // CertificationRevoked event + eDesc = NewEventDescriptor(CertificationRevokedEvent, + manifest.NewParameter("certID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + s.AddEvent(NewEvent(eDesc)) + + // CertificationRenewed event + eDesc = NewEventDescriptor(CertificationRenewedEvent, + manifest.NewParameter("certID", smartcontract.IntegerType), + manifest.NewParameter("newExpiresAt", smartcontract.IntegerType)) + s.AddEvent(NewEvent(eDesc)) + + return s +} + +// Metadata returns contract metadata. +func (s *Scire) Metadata() *interop.ContractMD { + return &s.ContractMD +} + +// Initialize initializes the Scire contract. +func (s *Scire) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != s.ActiveIn() { + return nil + } + + // Initialize counters + s.setAccountCounter(ic.DAO, 0) + s.setCertCounter(ic.DAO, 0) + s.setEnrollCounter(ic.DAO, 0) + + // Initialize config with defaults + cfg := &state.ScireConfig{ + AnnualCreditAllocation: 1000, // 1000 credits per year + MaxCreditsPerProgram: 500, // Max 500 credits per program + CertificationFee: 0, // Free certification + MinEnrollmentDuration: 86400, // ~1 day in blocks (1-second blocks) + } + s.setConfig(ic.DAO, cfg) + + // Initialize cache + cache := &ScireCache{ + accountCount: 0, + certCount: 0, + enrollCount: 0, + } + ic.DAO.SetCache(s.ID, cache) + + return nil +} + +// InitializeCache initializes the cache from storage. +func (s *Scire) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + cache := &ScireCache{ + accountCount: s.getAccountCounter(d), + certCount: s.getCertCounter(d), + enrollCount: s.getEnrollCounter(d), + } + d.SetCache(s.ID, cache) + return nil +} + +// OnPersist is called before block is committed. +func (s *Scire) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist is called after block is committed. +func (s *Scire) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn returns the hardfork at which this contract is activated. +func (s *Scire) ActiveIn() *config.Hardfork { + return nil // Always active +} + +// ===== Storage Helpers ===== + +func (s *Scire) makeAccountKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = scirePrefixAccount + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func (s *Scire) makeAccountByOwnerKey(owner util.Uint160) []byte { + key := make([]byte, 21) + key[0] = scirePrefixAccountByOwner + copy(key[1:], owner.BytesBE()) + return key +} + +func (s *Scire) makeCertificationKey(certID uint64) []byte { + key := make([]byte, 9) + key[0] = scirePrefixCertification + binary.BigEndian.PutUint64(key[1:], certID) + return key +} + +func (s *Scire) makeCertByOwnerKey(vitaID, certID uint64) []byte { + key := make([]byte, 17) + key[0] = scirePrefixCertByOwner + binary.BigEndian.PutUint64(key[1:9], vitaID) + binary.BigEndian.PutUint64(key[9:], certID) + return key +} + +func (s *Scire) makeEnrollmentKey(enrollID uint64) []byte { + key := make([]byte, 9) + key[0] = scirePrefixEnrollment + binary.BigEndian.PutUint64(key[1:], enrollID) + return key +} + +func (s *Scire) makeActiveEnrollmentKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = scirePrefixActiveEnrollment + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +// Counter getters/setters +func (s *Scire) getAccountCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{scirePrefixAccountCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Scire) setAccountCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{scirePrefixAccountCounter}, buf) +} + +func (s *Scire) getCertCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{scirePrefixCertCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Scire) setCertCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{scirePrefixCertCounter}, buf) +} + +func (s *Scire) getEnrollCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(s.ID, []byte{scirePrefixEnrollCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Scire) setEnrollCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(s.ID, []byte{scirePrefixEnrollCounter}, buf) +} + +// Config getter/setter +func (s *Scire) getConfigInternal(d *dao.Simple) *state.ScireConfig { + si := d.GetStorageItem(s.ID, []byte{scirePrefixConfig}) + if si == nil { + return &state.ScireConfig{ + AnnualCreditAllocation: 1000, + MaxCreditsPerProgram: 500, + CertificationFee: 0, + MinEnrollmentDuration: 86400, + } + } + cfg := new(state.ScireConfig) + item, _ := stackitem.Deserialize(si) + cfg.FromStackItem(item) + return cfg +} + +func (s *Scire) setConfig(d *dao.Simple, cfg *state.ScireConfig) { + item, _ := cfg.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, []byte{scirePrefixConfig}, data) +} + +// Account storage +func (s *Scire) getAccountInternal(d *dao.Simple, vitaID uint64) *state.EducationAccount { + si := d.GetStorageItem(s.ID, s.makeAccountKey(vitaID)) + if si == nil { + return nil + } + acc := new(state.EducationAccount) + item, _ := stackitem.Deserialize(si) + acc.FromStackItem(item) + return acc +} + +func (s *Scire) putAccount(d *dao.Simple, acc *state.EducationAccount) { + item, _ := acc.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeAccountKey(acc.VitaID), data) +} + +func (s *Scire) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { + si := d.GetStorageItem(s.ID, s.makeAccountByOwnerKey(owner)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (s *Scire) setOwnerToVitaID(d *dao.Simple, owner util.Uint160, vitaID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, vitaID) + d.PutStorageItem(s.ID, s.makeAccountByOwnerKey(owner), buf) +} + +// Enrollment storage +func (s *Scire) getEnrollmentInternal(d *dao.Simple, enrollID uint64) *state.Enrollment { + si := d.GetStorageItem(s.ID, s.makeEnrollmentKey(enrollID)) + if si == nil { + return nil + } + enroll := new(state.Enrollment) + item, _ := stackitem.Deserialize(si) + enroll.FromStackItem(item) + return enroll +} + +func (s *Scire) putEnrollment(d *dao.Simple, enroll *state.Enrollment) { + item, _ := enroll.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeEnrollmentKey(enroll.ID), data) +} + +func (s *Scire) getActiveEnrollmentID(d *dao.Simple, vitaID uint64) uint64 { + si := d.GetStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (s *Scire) setActiveEnrollmentID(d *dao.Simple, vitaID, enrollID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, enrollID) + d.PutStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID), buf) +} + +func (s *Scire) clearActiveEnrollment(d *dao.Simple, vitaID uint64) { + d.DeleteStorageItem(s.ID, s.makeActiveEnrollmentKey(vitaID)) +} + +// Certification storage +func (s *Scire) getCertificationInternal(d *dao.Simple, certID uint64) *state.Certification { + si := d.GetStorageItem(s.ID, s.makeCertificationKey(certID)) + if si == nil { + return nil + } + cert := new(state.Certification) + item, _ := stackitem.Deserialize(si) + cert.FromStackItem(item) + return cert +} + +func (s *Scire) putCertification(d *dao.Simple, cert *state.Certification) { + item, _ := cert.ToStackItem() + data, _ := stackitem.Serialize(item) + d.PutStorageItem(s.ID, s.makeCertificationKey(cert.ID), data) +} + +func (s *Scire) setCertByOwner(d *dao.Simple, vitaID, certID uint64) { + d.PutStorageItem(s.ID, s.makeCertByOwnerKey(vitaID, certID), []byte{1}) +} + +// ===== Contract Methods ===== + +// createAccount creates an education account for a Vita holder. +func (s *Scire) createAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + // Check owner has active Vita + if s.Vita == nil { + panic(ErrScireNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil { + panic(ErrScireNoVita) + } + if vita.Status != state.TokenStatusActive { + panic(ErrScireNoVita) + } + + // Check if account already exists + existing := s.getAccountInternal(ic.DAO, vita.TokenID) + if existing != nil { + panic(ErrScireAccountExists) + } + + // Check education rights + if !s.checkEducationRight(ic, owner) { + // Log but allow (EnforcementLogging) + // In the future, we could emit an event here + } + + // Get cache and increment counter + cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) + accountNum := cache.accountCount + cache.accountCount++ + s.setAccountCounter(ic.DAO, cache.accountCount) + + // Create account + acc := &state.EducationAccount{ + VitaID: vita.TokenID, + Owner: owner, + TotalCredits: 0, + UsedCredits: 0, + AvailableCredits: 0, + Status: state.EducationAccountActive, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + + // Store account + s.putAccount(ic.DAO, acc) + s.setOwnerToVitaID(ic.DAO, owner, vita.TokenID) + + // Emit event + ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), + stackitem.NewByteArray(owner.BytesBE()), + })) + + _ = accountNum // suppress unused warning + return stackitem.NewBool(true) +} + +// getAccount returns education account by owner. +func (s *Scire) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.Null{} + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.Null{} + } + + item, _ := acc.ToStackItem() + return item +} + +// getAccountByVitaID returns education account by Vita ID. +func (s *Scire) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { + vitaID := toUint64(args[0]) + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.Null{} + } + + item, _ := acc.ToStackItem() + return item +} + +// allocateCredits allocates learning credits to an account (committee only). +func (s *Scire) allocateCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + amount := toUint64(args[1]) + // reason := toString(args[2]) // for logging + + // Committee only + if !s.checkCommittee(ic) { + panic(ErrScireNotCommittee) + } + + if amount == 0 { + panic(ErrScireInvalidCredits) + } + + // Get or create account + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + // Auto-create account if Vita exists + if s.Vita == nil { + panic(ErrScireNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil || vita.Status != state.TokenStatusActive { + panic(ErrScireNoVita) + } + vitaID = vita.TokenID + + // Create account + cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) + cache.accountCount++ + s.setAccountCounter(ic.DAO, cache.accountCount) + + acc := &state.EducationAccount{ + VitaID: vitaID, + Owner: owner, + TotalCredits: amount, + UsedCredits: 0, + AvailableCredits: amount, + Status: state.EducationAccountActive, + CreatedAt: ic.Block.Index, + UpdatedAt: ic.Block.Index, + } + s.putAccount(ic.DAO, acc) + s.setOwnerToVitaID(ic.DAO, owner, vitaID) + + // Emit events + ic.AddNotification(s.Hash, AccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewByteArray(owner.BytesBE()), + })) + ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + })) + + return stackitem.NewBool(true) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + panic(ErrScireAccountNotFound) + } + if acc.Status != state.EducationAccountActive { + panic(ErrScireAccountSuspended) + } + + // Add credits + acc.TotalCredits += amount + acc.AvailableCredits += amount + acc.UpdatedAt = ic.Block.Index + + s.putAccount(ic.DAO, acc) + + // Emit event + ic.AddNotification(s.Hash, CreditsAllocatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewBigInteger(big.NewInt(int64(amount))), + stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))), + })) + + return stackitem.NewBool(true) +} + +// getCredits returns available credits for an owner. +func (s *Scire) getCredits(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + return stackitem.NewBigInteger(big.NewInt(int64(acc.AvailableCredits))) +} + +// enroll enrolls a student in an education program. +func (s *Scire) enroll(ic *interop.Context, args []stackitem.Item) stackitem.Item { + student := toUint160(args[0]) + programID := toString(args[1]) + institution := toUint160(args[2]) + credits := toUint64(args[3]) + + // Validate inputs + if len(programID) == 0 || len(programID) > 128 { + panic(ErrScireInvalidProgramID) + } + if credits == 0 { + panic(ErrScireInvalidCredits) + } + + // Check max credits + cfg := s.getConfigInternal(ic.DAO) + if credits > cfg.MaxCreditsPerProgram { + panic(ErrScireExceedsMaxCredits) + } + + // Get student's account + vitaID, found := s.getVitaIDByOwner(ic.DAO, student) + if !found { + panic(ErrScireAccountNotFound) + } + + acc := s.getAccountInternal(ic.DAO, vitaID) + if acc == nil { + panic(ErrScireAccountNotFound) + } + if acc.Status != state.EducationAccountActive { + panic(ErrScireAccountSuspended) + } + + // Check sufficient credits + if acc.AvailableCredits < credits { + panic(ErrScireInsufficientCredits) + } + + // Check not already enrolled + activeEnrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) + if activeEnrollID != 0 { + panic(ErrScireAlreadyEnrolled) + } + + // Check education rights + if !s.checkEducationRight(ic, student) { + // Log but allow + } + + // Get next enrollment ID + cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) + enrollID := cache.enrollCount + cache.enrollCount++ + s.setEnrollCounter(ic.DAO, cache.enrollCount) + + // Deduct credits + acc.UsedCredits += credits + acc.AvailableCredits -= credits + acc.UpdatedAt = ic.Block.Index + s.putAccount(ic.DAO, acc) + + // Create enrollment + enroll := &state.Enrollment{ + ID: enrollID, + VitaID: vitaID, + Student: student, + ProgramID: programID, + Institution: institution, + CreditsAllocated: credits, + StartedAt: ic.Block.Index, + CompletedAt: 0, + Status: state.EnrollmentActive, + } + + s.putEnrollment(ic.DAO, enroll) + s.setActiveEnrollmentID(ic.DAO, vitaID, enrollID) + + // Emit event + ic.AddNotification(s.Hash, EnrollmentCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(enrollID))), + stackitem.NewBigInteger(big.NewInt(int64(vitaID))), + stackitem.NewByteArray([]byte(programID)), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(enrollID))) +} + +// completeEnrollment marks an enrollment as completed (institution/educator only). +func (s *Scire) completeEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + enrollID := toUint64(args[0]) + // contentHash := toUint256(args[1]) // for proof storage + + enroll := s.getEnrollmentInternal(ic.DAO, enrollID) + if enroll == nil { + panic(ErrScireEnrollNotFound) + } + if enroll.Status != state.EnrollmentActive { + panic(ErrScireEnrollNotActive) + } + + // Check caller is institution or educator + caller := ic.VM.GetCallingScriptHash() + if caller != enroll.Institution && !s.checkEducator(ic) { + panic(ErrScireNotInstitution) + } + + // Update enrollment + enroll.Status = state.EnrollmentCompleted + enroll.CompletedAt = ic.Block.Index + s.putEnrollment(ic.DAO, enroll) + + // Clear active enrollment + s.clearActiveEnrollment(ic.DAO, enroll.VitaID) + + // Emit event + ic.AddNotification(s.Hash, EnrollmentCompletedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(enrollID))), + stackitem.NewBigInteger(big.NewInt(int64(enroll.VitaID))), + })) + + return stackitem.NewBool(true) +} + +// withdrawEnrollment withdraws from an enrollment. +func (s *Scire) withdrawEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + enrollID := toUint64(args[0]) + reason := toString(args[1]) + + enroll := s.getEnrollmentInternal(ic.DAO, enrollID) + if enroll == nil { + panic(ErrScireEnrollNotFound) + } + if enroll.Status != state.EnrollmentActive { + panic(ErrScireEnrollNotActive) + } + + // Check caller is student or institution + caller := ic.VM.GetCallingScriptHash() + if caller != enroll.Student && caller != enroll.Institution && !s.checkEducator(ic) { + panic(ErrScireNotStudent) + } + + // Update enrollment + enroll.Status = state.EnrollmentWithdrawn + enroll.CompletedAt = ic.Block.Index + s.putEnrollment(ic.DAO, enroll) + + // Clear active enrollment + s.clearActiveEnrollment(ic.DAO, enroll.VitaID) + + // Partial refund: return 50% of credits + acc := s.getAccountInternal(ic.DAO, enroll.VitaID) + if acc != nil { + refund := enroll.CreditsAllocated / 2 + acc.AvailableCredits += refund + acc.UsedCredits -= refund + acc.UpdatedAt = ic.Block.Index + s.putAccount(ic.DAO, acc) + } + + // Emit event + ic.AddNotification(s.Hash, EnrollmentWithdrawnEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(enrollID))), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// getEnrollment returns enrollment details. +func (s *Scire) getEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + enrollID := toUint64(args[0]) + + enroll := s.getEnrollmentInternal(ic.DAO, enrollID) + if enroll == nil { + return stackitem.Null{} + } + + item, _ := enroll.ToStackItem() + return item +} + +// getActiveEnrollment returns a student's active enrollment. +func (s *Scire) getActiveEnrollment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + student := toUint160(args[0]) + + vitaID, found := s.getVitaIDByOwner(ic.DAO, student) + if !found { + return stackitem.Null{} + } + + enrollID := s.getActiveEnrollmentID(ic.DAO, vitaID) + if enrollID == 0 { + return stackitem.Null{} + } + + enroll := s.getEnrollmentInternal(ic.DAO, enrollID) + if enroll == nil { + return stackitem.Null{} + } + + item, _ := enroll.ToStackItem() + return item +} + +// issueCertification issues a certification (educator only). +func (s *Scire) issueCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + certType := toString(args[1]) + name := toString(args[2]) + contentHashBytes := toBytes(args[3]) + expiresAt := toUint32(args[4]) + + // Convert bytes to Uint256 + var contentHash util.Uint256 + if len(contentHashBytes) == 32 { + copy(contentHash[:], contentHashBytes) + } + + // Validate inputs + if len(certType) == 0 || len(certType) > 64 { + panic(ErrScireInvalidCertType) + } + if len(name) == 0 || len(name) > 128 { + panic(ErrScireInvalidName) + } + + // Check educator authority + if !s.checkEducator(ic) { + panic(ErrScireNotEducator) + } + + // Get owner's Vita + if s.Vita == nil { + panic(ErrScireNoVita) + } + vita, err := s.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil || vita.Status != state.TokenStatusActive { + panic(ErrScireNoVita) + } + + // Get issuing institution + institution := ic.VM.GetCallingScriptHash() + + // Get next cert ID + cache := ic.DAO.GetRWCache(s.ID).(*ScireCache) + certID := cache.certCount + cache.certCount++ + s.setCertCounter(ic.DAO, cache.certCount) + + // Create certification + cert := &state.Certification{ + ID: certID, + VitaID: vita.TokenID, + Owner: owner, + CertType: certType, + Name: name, + Institution: institution, + ContentHash: contentHash, + IssuedAt: ic.Block.Index, + ExpiresAt: expiresAt, + Status: state.CertificationActive, + } + + s.putCertification(ic.DAO, cert) + s.setCertByOwner(ic.DAO, vita.TokenID, certID) + + // Emit event + ic.AddNotification(s.Hash, CertificationIssuedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(certID))), + stackitem.NewBigInteger(big.NewInt(int64(vita.TokenID))), + stackitem.NewByteArray([]byte(certType)), + })) + + return stackitem.NewBigInteger(big.NewInt(int64(certID))) +} + +// revokeCertification revokes a certification (institution only). +func (s *Scire) revokeCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + certID := toUint64(args[0]) + reason := toString(args[1]) + + cert := s.getCertificationInternal(ic.DAO, certID) + if cert == nil { + panic(ErrScireCertNotFound) + } + if cert.Status == state.CertificationRevoked { + panic(ErrScireCertRevoked) + } + + // Check caller is issuing institution or educator + caller := ic.VM.GetCallingScriptHash() + if caller != cert.Institution && !s.checkEducator(ic) { + panic(ErrScireNotInstitution) + } + + // Revoke + cert.Status = state.CertificationRevoked + s.putCertification(ic.DAO, cert) + + // Emit event + ic.AddNotification(s.Hash, CertificationRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(certID))), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// renewCertification extends a certification's expiry. +func (s *Scire) renewCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + certID := toUint64(args[0]) + newExpiresAt := toUint32(args[1]) + + cert := s.getCertificationInternal(ic.DAO, certID) + if cert == nil { + panic(ErrScireCertNotFound) + } + if cert.Status == state.CertificationRevoked { + panic(ErrScireCertRevoked) + } + + // Check caller is issuing institution or educator + caller := ic.VM.GetCallingScriptHash() + if caller != cert.Institution && !s.checkEducator(ic) { + panic(ErrScireNotInstitution) + } + + // Update expiry + cert.ExpiresAt = newExpiresAt + cert.Status = state.CertificationActive // Reactivate if was expired + s.putCertification(ic.DAO, cert) + + // Emit event + ic.AddNotification(s.Hash, CertificationRenewedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(certID))), + stackitem.NewBigInteger(big.NewInt(int64(newExpiresAt))), + })) + + return stackitem.NewBool(true) +} + +// getCertification returns certification details. +func (s *Scire) getCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + certID := toUint64(args[0]) + + cert := s.getCertificationInternal(ic.DAO, certID) + if cert == nil { + return stackitem.Null{} + } + + item, _ := cert.ToStackItem() + return item +} + +// verifyCertification checks if a certification is currently valid. +func (s *Scire) verifyCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + certID := toUint64(args[0]) + + cert := s.getCertificationInternal(ic.DAO, certID) + if cert == nil { + return stackitem.NewBool(false) + } + + return stackitem.NewBool(cert.IsValid(ic.Block.Index)) +} + +// hasCertification checks if an owner has a specific certification type. +func (s *Scire) hasCertification(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + certType := toString(args[1]) + + vitaID, exists := s.getVitaIDByOwner(ic.DAO, owner) + if !exists { + return stackitem.NewBool(false) + } + + // Scan certifications for this owner + prefix := make([]byte, 9) + prefix[0] = scirePrefixCertByOwner + binary.BigEndian.PutUint64(prefix[1:], vitaID) + + var hasCert bool + ic.DAO.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 8 { + certID := binary.BigEndian.Uint64(k[len(k)-8:]) + cert := s.getCertificationInternal(ic.DAO, certID) + if cert != nil && cert.CertType == certType && cert.IsValid(ic.Block.Index) { + hasCert = true + return false // Stop iteration + } + } + return true + }) + + return stackitem.NewBool(hasCert) +} + +// getConfig returns the Scire configuration. +func (s *Scire) getConfig(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cfg := s.getConfigInternal(ic.DAO) + item, _ := cfg.ToStackItem() + return item +} + +// getTotalAccounts returns the total number of education accounts. +func (s *Scire) getTotalAccounts(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*ScireCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.accountCount))) +} + +// getTotalCertifications returns the total number of certifications. +func (s *Scire) getTotalCertifications(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*ScireCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.certCount))) +} + +// getTotalEnrollments returns the total number of enrollments. +func (s *Scire) getTotalEnrollments(ic *interop.Context, args []stackitem.Item) stackitem.Item { + cache := ic.DAO.GetROCache(s.ID).(*ScireCache) + return stackitem.NewBigInteger(big.NewInt(int64(cache.enrollCount))) +} + +// ===== Public Interface Methods for Cross-Contract Access ===== + +// GetAccountByOwner returns an education account by owner address. +func (s *Scire) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.EducationAccount, error) { + vitaID, found := s.getVitaIDByOwner(d, owner) + if !found { + return nil, ErrScireAccountNotFound + } + acc := s.getAccountInternal(d, vitaID) + if acc == nil { + return nil, ErrScireAccountNotFound + } + return acc, nil +} + +// HasValidCertification checks if owner has a valid certification of the given type. +func (s *Scire) HasValidCertification(d *dao.Simple, owner util.Uint160, certType string, blockHeight uint32) bool { + vitaID, exists := s.getVitaIDByOwner(d, owner) + if !exists { + return false + } + + prefix := make([]byte, 9) + prefix[0] = scirePrefixCertByOwner + binary.BigEndian.PutUint64(prefix[1:], vitaID) + + var hasCert bool + d.Seek(s.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + if len(k) >= 8 { + certID := binary.BigEndian.Uint64(k[len(k)-8:]) + cert := s.getCertificationInternal(d, certID) + if cert != nil && cert.CertType == certType { + if cert.Status == state.CertificationActive { + if cert.ExpiresAt == 0 || cert.ExpiresAt > blockHeight { + hasCert = true + return false + } + } + } + } + return true + }) + + return hasCert +} + +// Address returns the contract's script hash. +func (s *Scire) Address() util.Uint160 { + return s.Hash +} diff --git a/pkg/core/native/tribute.go b/pkg/core/native/tribute.go old mode 100644 new mode 100755 index 873bd7f..b33ca2c --- a/pkg/core/native/tribute.go +++ b/pkg/core/native/tribute.go @@ -1,1647 +1,1647 @@ -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/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" -) - -// Tribute represents the anti-hoarding economics native contract. -type Tribute struct { - interop.ContractMD - Tutus ITutus - Vita IVita - VTS IVTS - RoleRegistry IRoleRegistry - Lex ILex -} - -// TributeCache represents the cached state for Tribute contract. -type TributeCache struct { - accountCount uint64 - assessmentCount uint64 - incentiveCount uint64 - redistributionCount uint64 -} - -// Storage key prefixes for Tribute. -const ( - tributePrefixAccount byte = 0x01 // vitaID -> VelocityAccount - tributePrefixAccountByOwner byte = 0x02 // owner -> vitaID - tributePrefixAssessment byte = 0x10 // assessmentID -> TributeAssessment - tributePrefixAssessmentByOwner byte = 0x11 // vitaID + assessmentID -> exists - tributePrefixPendingAssessment byte = 0x12 // vitaID -> latest pending assessmentID - tributePrefixIncentive byte = 0x20 // incentiveID -> CirculationIncentive - tributePrefixIncentiveByOwner byte = 0x21 // vitaID + incentiveID -> exists - tributePrefixUnclaimedIncentive byte = 0x22 // vitaID + incentiveID -> exists (unclaimed only) - tributePrefixRedistribution byte = 0x30 // redistID -> RedistributionRecord - tributePrefixAccountCounter byte = 0xF0 // -> uint64 - tributePrefixAssessmentCounter byte = 0xF1 // -> next assessment ID - tributePrefixIncentiveCounter byte = 0xF2 // -> next incentive ID - tributePrefixRedistributionCtr byte = 0xF3 // -> next redistribution ID - tributePrefixTotalTributePool byte = 0xF8 // -> total tribute collected for redistribution - tributePrefixConfig byte = 0xFF // -> TributeConfig -) - -// Event names for Tribute. -const ( - VelocityAccountCreatedEvent = "VelocityAccountCreated" - VelocityUpdatedEvent = "VelocityUpdated" - TributeAssessedEvent = "TributeAssessed" - TributeCollectedEvent = "TributeCollected" - TributeWaivedEvent = "TributeWaived" - TributeAppealedEvent = "TributeAppealed" - IncentiveGrantedEvent = "IncentiveGranted" - IncentiveClaimedEvent = "IncentiveClaimed" - RedistributionExecutedEvent = "RedistributionExecuted" - ExemptionGrantedEvent = "ExemptionGranted" - ExemptionRevokedEvent = "ExemptionRevoked" -) - -// Role constants for tribute administrators. -const ( - RoleTributeAdmin uint64 = 23 // Can manage exemptions and appeals -) - -// Various errors for Tribute. -var ( - ErrTributeAccountNotFound = errors.New("velocity account not found") - ErrTributeAccountExists = errors.New("velocity account already exists") - ErrTributeAccountExempt = errors.New("account is exempt from tribute") - ErrTributeAccountSuspended = errors.New("velocity account is suspended") - ErrTributeNoVita = errors.New("owner must have an active Vita") - ErrTributeAssessmentNotFound = errors.New("tribute assessment not found") - ErrTributeAssessmentNotPending = errors.New("assessment is not pending") - ErrTributeAssessmentAlreadyPaid = errors.New("assessment already collected") - ErrTributeIncentiveNotFound = errors.New("incentive not found") - ErrTributeIncentiveClaimed = errors.New("incentive already claimed") - ErrTributeInsufficientBalance = errors.New("insufficient balance for tribute") - ErrTributeNotCommittee = errors.New("invalid committee signature") - ErrTributeNotOwner = errors.New("caller is not the owner") - ErrTributeNotAdmin = errors.New("caller is not an authorized tribute admin") - ErrTributePropertyRestricted = errors.New("property right is restricted") - ErrTributeInvalidAmount = errors.New("invalid amount") - ErrTributeInvalidReason = errors.New("invalid reason") - ErrTributeBelowExemption = errors.New("balance below exemption threshold") - ErrTributeNoHoarding = errors.New("no hoarding detected") - ErrTributeNothingToRedistribute = errors.New("nothing to redistribute") -) - -var ( - _ interop.Contract = (*Tribute)(nil) - _ dao.NativeContractCache = (*TributeCache)(nil) -) - -// Copy implements NativeContractCache interface. -func (c *TributeCache) Copy() dao.NativeContractCache { - return &TributeCache{ - accountCount: c.accountCount, - assessmentCount: c.assessmentCount, - incentiveCount: c.incentiveCount, - redistributionCount: c.redistributionCount, - } -} - -// checkCommittee checks if the caller has committee authority. -func (t *Tribute) checkCommittee(ic *interop.Context) bool { - if t.RoleRegistry != nil { - return t.RoleRegistry.CheckCommittee(ic) - } - return t.Tutus.CheckCommittee(ic) -} - -// checkTributeAdmin checks if the caller has tribute admin authority. -func (t *Tribute) checkTributeAdmin(ic *interop.Context) bool { - caller := ic.VM.GetCallingScriptHash() - if t.RoleRegistry != nil { - if t.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleTributeAdmin, ic.Block.Index) { - return true - } - } - // Committee members can also act as tribute admins - return t.checkCommittee(ic) -} - -// checkPropertyRight checks if subject has property rights via Lex. -func (t *Tribute) checkPropertyRight(ic *interop.Context, subject util.Uint160) bool { - if t.Lex == nil { - return true // Allow if Lex not available - } - return t.Lex.HasRightInternal(ic.DAO, subject, state.RightProperty, ic.Block.Index) -} - -// newTribute creates a new Tribute native contract. -func newTribute() *Tribute { - t := &Tribute{ - ContractMD: *interop.NewContractMD(nativenames.Tribute, nativeids.Tribute), - } - defer t.BuildHFSpecificMD(t.ActiveIn()) - - // ===== Account Management ===== - - // createVelocityAccount - Create velocity tracking account for a Vita holder - desc := NewDescriptor("createVelocityAccount", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md := NewMethodAndPrice(t.createVelocityAccount, 1<<17, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // getAccount - Get velocity account by owner - desc = NewDescriptor("getAccount", smartcontract.ArrayType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.getAccount, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getAccountByVitaID - Get account by Vita ID - desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, - manifest.NewParameter("vitaID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.getAccountByVitaID, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // recordTransaction - Record a transaction for velocity tracking - desc = NewDescriptor("recordTransaction", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("isOutflow", smartcontract.BoolType)) - md = NewMethodAndPrice(t.recordTransaction, 1<<16, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // getVelocity - Get current velocity score for an owner - desc = NewDescriptor("getVelocity", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.getVelocity, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getHoardingLevel - Get current hoarding level for an owner - desc = NewDescriptor("getHoardingLevel", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.getHoardingLevel, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // ===== Exemption Management ===== - - // grantExemption - Grant exemption from tribute (admin only) - desc = NewDescriptor("grantExemption", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(t.grantExemption, 1<<16, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // revokeExemption - Revoke exemption from tribute (admin only) - desc = NewDescriptor("revokeExemption", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.revokeExemption, 1<<16, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // isExempt - Check if account is exempt - desc = NewDescriptor("isExempt", smartcontract.BoolType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.isExempt, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // ===== Assessment Management ===== - - // assessTribute - Assess tribute for hoarding (called periodically or on-demand) - desc = NewDescriptor("assessTribute", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.assessTribute, 1<<17, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // getAssessment - Get assessment by ID - desc = NewDescriptor("getAssessment", smartcontract.ArrayType, - manifest.NewParameter("assessmentID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.getAssessment, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getPendingAssessment - Get pending assessment for an owner - desc = NewDescriptor("getPendingAssessment", smartcontract.ArrayType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.getPendingAssessment, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // collectTribute - Collect pending tribute - desc = NewDescriptor("collectTribute", smartcontract.BoolType, - manifest.NewParameter("assessmentID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.collectTribute, 1<<17, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // waiveTribute - Waive a tribute assessment (admin only) - desc = NewDescriptor("waiveTribute", smartcontract.BoolType, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(t.waiveTribute, 1<<16, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // appealTribute - Appeal a tribute assessment - desc = NewDescriptor("appealTribute", smartcontract.BoolType, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(t.appealTribute, 1<<16, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // ===== Incentive Management ===== - - // grantIncentive - Grant circulation incentive (system/admin) - desc = NewDescriptor("grantIncentive", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type), - manifest.NewParameter("incentiveType", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - md = NewMethodAndPrice(t.grantIncentive, 1<<17, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // claimIncentive - Claim a granted incentive - desc = NewDescriptor("claimIncentive", smartcontract.BoolType, - manifest.NewParameter("incentiveID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.claimIncentive, 1<<17, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // getIncentive - Get incentive by ID - desc = NewDescriptor("getIncentive", smartcontract.ArrayType, - manifest.NewParameter("incentiveID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.getIncentive, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getUnclaimedIncentives - Get count of unclaimed incentives for owner - desc = NewDescriptor("getUnclaimedIncentives", smartcontract.IntegerType, - manifest.NewParameter("owner", smartcontract.Hash160Type)) - md = NewMethodAndPrice(t.getUnclaimedIncentives, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // ===== Redistribution ===== - - // redistribute - Execute wealth redistribution (committee only) - desc = NewDescriptor("redistribute", smartcontract.IntegerType, - manifest.NewParameter("targetCategory", smartcontract.StringType), - manifest.NewParameter("recipientCount", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.redistribute, 1<<18, callflag.States|callflag.AllowNotify) - t.AddMethod(md, desc) - - // getRedistribution - Get redistribution record by ID - desc = NewDescriptor("getRedistribution", smartcontract.ArrayType, - manifest.NewParameter("redistID", smartcontract.IntegerType)) - md = NewMethodAndPrice(t.getRedistribution, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getTributePool - Get total tribute pool available for redistribution - desc = NewDescriptor("getTributePool", smartcontract.IntegerType) - md = NewMethodAndPrice(t.getTributePool, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // ===== Configuration & Stats ===== - - // getConfig - Get current configuration - desc = NewDescriptor("getConfig", smartcontract.ArrayType) - md = NewMethodAndPrice(t.getConfig, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getTotalAccounts - Get total velocity accounts - desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) - md = NewMethodAndPrice(t.getTotalAccounts, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getTotalAssessments - Get total assessments - desc = NewDescriptor("getTotalAssessments", smartcontract.IntegerType) - md = NewMethodAndPrice(t.getTotalAssessments, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getTotalIncentives - Get total incentives - desc = NewDescriptor("getTotalIncentives", smartcontract.IntegerType) - md = NewMethodAndPrice(t.getTotalIncentives, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // getTotalRedistributions - Get total redistributions - desc = NewDescriptor("getTotalRedistributions", smartcontract.IntegerType) - md = NewMethodAndPrice(t.getTotalRedistributions, 1<<15, callflag.ReadStates) - t.AddMethod(md, desc) - - // ===== Events ===== - - // VelocityAccountCreated event - eDesc := NewEventDescriptor(VelocityAccountCreatedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("owner", smartcontract.Hash160Type)) - t.AddEvent(NewEvent(eDesc)) - - // VelocityUpdated event - eDesc = NewEventDescriptor(VelocityUpdatedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("newVelocity", smartcontract.IntegerType), - manifest.NewParameter("hoardingLevel", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // TributeAssessed event - eDesc = NewEventDescriptor(TributeAssessedEvent, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // TributeCollected event - eDesc = NewEventDescriptor(TributeCollectedEvent, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // TributeWaived event - eDesc = NewEventDescriptor(TributeWaivedEvent, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - t.AddEvent(NewEvent(eDesc)) - - // TributeAppealed event - eDesc = NewEventDescriptor(TributeAppealedEvent, - manifest.NewParameter("assessmentID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - t.AddEvent(NewEvent(eDesc)) - - // IncentiveGranted event - eDesc = NewEventDescriptor(IncentiveGrantedEvent, - manifest.NewParameter("incentiveID", smartcontract.IntegerType), - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // IncentiveClaimed event - eDesc = NewEventDescriptor(IncentiveClaimedEvent, - manifest.NewParameter("incentiveID", smartcontract.IntegerType), - manifest.NewParameter("amount", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // RedistributionExecuted event - eDesc = NewEventDescriptor(RedistributionExecutedEvent, - manifest.NewParameter("redistID", smartcontract.IntegerType), - manifest.NewParameter("totalAmount", smartcontract.IntegerType), - manifest.NewParameter("recipientCount", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - // ExemptionGranted event - eDesc = NewEventDescriptor(ExemptionGrantedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType), - manifest.NewParameter("reason", smartcontract.StringType)) - t.AddEvent(NewEvent(eDesc)) - - // ExemptionRevoked event - eDesc = NewEventDescriptor(ExemptionRevokedEvent, - manifest.NewParameter("vitaID", smartcontract.IntegerType)) - t.AddEvent(NewEvent(eDesc)) - - return t -} - -// Metadata returns contract metadata. -func (t *Tribute) Metadata() *interop.ContractMD { - return &t.ContractMD -} - -// Initialize initializes the Tribute contract. -func (t *Tribute) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { - if hf != t.ActiveIn() { - return nil - } - - // Initialize counters - t.setAccountCounter(ic.DAO, 0) - t.setAssessmentCounter(ic.DAO, 0) - t.setIncentiveCounter(ic.DAO, 0) - t.setRedistributionCounter(ic.DAO, 0) - t.setTributePool(ic.DAO, 0) - - // Initialize config with defaults - // Velocity in basis points (0-10000 = 0%-100%) - cfg := &state.TributeConfig{ - VelocityThresholdMild: 5000, // Below 50% velocity = mild hoarding - VelocityThresholdModerate: 3000, // Below 30% = moderate hoarding - VelocityThresholdSevere: 1500, // Below 15% = severe hoarding - VelocityThresholdExtreme: 500, // Below 5% = extreme hoarding - TributeRateMild: 100, // 1% tribute for mild hoarding - TributeRateModerate: 300, // 3% tribute for moderate hoarding - TributeRateSevere: 700, // 7% tribute for severe hoarding - TributeRateExtreme: 1500, // 15% tribute for extreme hoarding - IncentiveRateHigh: 50, // 0.5% incentive for high velocity - IncentiveRateVeryHigh: 150, // 1.5% incentive for very high velocity - StagnancyPeriod: 86400, // ~1 day (1-second blocks) before balance is stagnant - AssessmentPeriod: 604800, // ~7 days between assessments - GracePeriod: 259200, // ~3 days to pay tribute - MinBalanceForTribute: 1000000, // 1 VTS minimum to assess - ExemptionThreshold: 100000, // 0.1 VTS exempt - } - t.setConfig(ic.DAO, cfg) - - // Initialize cache - cache := &TributeCache{ - accountCount: 0, - assessmentCount: 0, - incentiveCount: 0, - redistributionCount: 0, - } - ic.DAO.SetCache(t.ID, cache) - - return nil -} - -// InitializeCache initializes the cache from storage. -func (t *Tribute) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { - cache := &TributeCache{ - accountCount: t.getAccountCounter(d), - assessmentCount: t.getAssessmentCounter(d), - incentiveCount: t.getIncentiveCounter(d), - redistributionCount: t.getRedistributionCounter(d), - } - d.SetCache(t.ID, cache) - return nil -} - -// OnPersist is called before block is committed. -func (t *Tribute) OnPersist(ic *interop.Context) error { - return nil -} - -// PostPersist is called after block is committed. -func (t *Tribute) PostPersist(ic *interop.Context) error { - return nil -} - -// ActiveIn returns the hardfork at which this contract is activated. -func (t *Tribute) ActiveIn() *config.Hardfork { - return nil // Always active -} - -// ===== Storage Helpers ===== - -func (t *Tribute) makeAccountKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = tributePrefixAccount - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -func (t *Tribute) makeAccountByOwnerKey(owner util.Uint160) []byte { - key := make([]byte, 21) - key[0] = tributePrefixAccountByOwner - copy(key[1:], owner.BytesBE()) - return key -} - -func (t *Tribute) makeAssessmentKey(assessmentID uint64) []byte { - key := make([]byte, 9) - key[0] = tributePrefixAssessment - binary.BigEndian.PutUint64(key[1:], assessmentID) - return key -} - -func (t *Tribute) makePendingAssessmentKey(vitaID uint64) []byte { - key := make([]byte, 9) - key[0] = tributePrefixPendingAssessment - binary.BigEndian.PutUint64(key[1:], vitaID) - return key -} - -func (t *Tribute) makeIncentiveKey(incentiveID uint64) []byte { - key := make([]byte, 9) - key[0] = tributePrefixIncentive - binary.BigEndian.PutUint64(key[1:], incentiveID) - return key -} - -func (t *Tribute) makeUnclaimedIncentiveKey(vitaID, incentiveID uint64) []byte { - key := make([]byte, 17) - key[0] = tributePrefixUnclaimedIncentive - binary.BigEndian.PutUint64(key[1:], vitaID) - binary.BigEndian.PutUint64(key[9:], incentiveID) - return key -} - -func (t *Tribute) makeRedistributionKey(redistID uint64) []byte { - key := make([]byte, 9) - key[0] = tributePrefixRedistribution - binary.BigEndian.PutUint64(key[1:], redistID) - return key -} - -// ===== Counter Helpers ===== - -func (t *Tribute) getAccountCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(t.ID, []byte{tributePrefixAccountCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (t *Tribute) setAccountCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(t.ID, []byte{tributePrefixAccountCounter}, buf) -} - -func (t *Tribute) getAssessmentCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(t.ID, []byte{tributePrefixAssessmentCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (t *Tribute) setAssessmentCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(t.ID, []byte{tributePrefixAssessmentCounter}, buf) -} - -func (t *Tribute) getIncentiveCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(t.ID, []byte{tributePrefixIncentiveCounter}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (t *Tribute) setIncentiveCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(t.ID, []byte{tributePrefixIncentiveCounter}, buf) -} - -func (t *Tribute) getRedistributionCounter(d *dao.Simple) uint64 { - si := d.GetStorageItem(t.ID, []byte{tributePrefixRedistributionCtr}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (t *Tribute) setRedistributionCounter(d *dao.Simple, count uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, count) - d.PutStorageItem(t.ID, []byte{tributePrefixRedistributionCtr}, buf) -} - -func (t *Tribute) getTributePoolValue(d *dao.Simple) uint64 { - si := d.GetStorageItem(t.ID, []byte{tributePrefixTotalTributePool}) - if si == nil { - return 0 - } - return binary.BigEndian.Uint64(si) -} - -func (t *Tribute) setTributePool(d *dao.Simple, amount uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, amount) - d.PutStorageItem(t.ID, []byte{tributePrefixTotalTributePool}, buf) -} - -// ===== Account Storage ===== - -func (t *Tribute) getAccountInternal(d *dao.Simple, vitaID uint64) (*state.VelocityAccount, error) { - si := d.GetStorageItem(t.ID, t.makeAccountKey(vitaID)) - if si == nil { - return nil, nil - } - acc := new(state.VelocityAccount) - item, err := stackitem.Deserialize(si) - if err != nil { - return nil, err - } - if err := acc.FromStackItem(item); err != nil { - return nil, err - } - return acc, nil -} - -func (t *Tribute) putAccount(d *dao.Simple, acc *state.VelocityAccount) error { - data, err := stackitem.Serialize(acc.ToStackItem()) - if err != nil { - return err - } - d.PutStorageItem(t.ID, t.makeAccountKey(acc.VitaID), data) - return nil -} - -func (t *Tribute) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { - si := d.GetStorageItem(t.ID, t.makeAccountByOwnerKey(owner)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (t *Tribute) setOwnerMapping(d *dao.Simple, owner util.Uint160, vitaID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, vitaID) - d.PutStorageItem(t.ID, t.makeAccountByOwnerKey(owner), buf) -} - -// ===== Assessment Storage ===== - -func (t *Tribute) getAssessmentInternal(d *dao.Simple, assessmentID uint64) (*state.TributeAssessment, error) { - si := d.GetStorageItem(t.ID, t.makeAssessmentKey(assessmentID)) - if si == nil { - return nil, nil - } - assess := new(state.TributeAssessment) - item, err := stackitem.Deserialize(si) - if err != nil { - return nil, err - } - if err := assess.FromStackItem(item); err != nil { - return nil, err - } - return assess, nil -} - -func (t *Tribute) putAssessment(d *dao.Simple, assess *state.TributeAssessment) error { - data, err := stackitem.Serialize(assess.ToStackItem()) - if err != nil { - return err - } - d.PutStorageItem(t.ID, t.makeAssessmentKey(assess.ID), data) - return nil -} - -func (t *Tribute) getPendingAssessmentID(d *dao.Simple, vitaID uint64) (uint64, bool) { - si := d.GetStorageItem(t.ID, t.makePendingAssessmentKey(vitaID)) - if si == nil { - return 0, false - } - return binary.BigEndian.Uint64(si), true -} - -func (t *Tribute) setPendingAssessmentID(d *dao.Simple, vitaID, assessmentID uint64) { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, assessmentID) - d.PutStorageItem(t.ID, t.makePendingAssessmentKey(vitaID), buf) -} - -func (t *Tribute) deletePendingAssessment(d *dao.Simple, vitaID uint64) { - d.DeleteStorageItem(t.ID, t.makePendingAssessmentKey(vitaID)) -} - -// ===== Incentive Storage ===== - -func (t *Tribute) getIncentiveInternal(d *dao.Simple, incentiveID uint64) (*state.CirculationIncentive, error) { - si := d.GetStorageItem(t.ID, t.makeIncentiveKey(incentiveID)) - if si == nil { - return nil, nil - } - inc := new(state.CirculationIncentive) - item, err := stackitem.Deserialize(si) - if err != nil { - return nil, err - } - if err := inc.FromStackItem(item); err != nil { - return nil, err - } - return inc, nil -} - -func (t *Tribute) putIncentive(d *dao.Simple, inc *state.CirculationIncentive) error { - data, err := stackitem.Serialize(inc.ToStackItem()) - if err != nil { - return err - } - d.PutStorageItem(t.ID, t.makeIncentiveKey(inc.ID), data) - return nil -} - -func (t *Tribute) addUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) { - d.PutStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID), []byte{1}) -} - -func (t *Tribute) removeUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) { - d.DeleteStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID)) -} - -// ===== Redistribution Storage ===== - -func (t *Tribute) getRedistributionInternal(d *dao.Simple, redistID uint64) (*state.RedistributionRecord, error) { - si := d.GetStorageItem(t.ID, t.makeRedistributionKey(redistID)) - if si == nil { - return nil, nil - } - rec := new(state.RedistributionRecord) - item, err := stackitem.Deserialize(si) - if err != nil { - return nil, err - } - if err := rec.FromStackItem(item); err != nil { - return nil, err - } - return rec, nil -} - -func (t *Tribute) putRedistribution(d *dao.Simple, rec *state.RedistributionRecord) error { - data, err := stackitem.Serialize(rec.ToStackItem()) - if err != nil { - return err - } - d.PutStorageItem(t.ID, t.makeRedistributionKey(rec.ID), data) - return nil -} - -// ===== Config Storage ===== - -func (t *Tribute) getConfigInternal(d *dao.Simple) *state.TributeConfig { - si := d.GetStorageItem(t.ID, []byte{tributePrefixConfig}) - if si == nil { - return nil - } - cfg := new(state.TributeConfig) - item, err := stackitem.Deserialize(si) - if err != nil { - return nil - } - if err := cfg.FromStackItem(item); err != nil { - return nil - } - return cfg -} - -func (t *Tribute) setConfig(d *dao.Simple, cfg *state.TributeConfig) { - data, _ := stackitem.Serialize(cfg.ToStackItem()) - d.PutStorageItem(t.ID, []byte{tributePrefixConfig}, data) -} - -// ===== Internal Helpers ===== - -// calculateVelocity calculates velocity based on inflow/outflow ratio. -func (t *Tribute) calculateVelocity(totalInflow, totalOutflow uint64) uint64 { - if totalInflow == 0 { - if totalOutflow > 0 { - return 10000 // 100% velocity if only outflow - } - return 5000 // Default 50% if no activity - } - // Velocity = (outflow / inflow) * 10000 (basis points) - velocity := (totalOutflow * 10000) / totalInflow - if velocity > 10000 { - velocity = 10000 - } - return velocity -} - -// determineHoardingLevel determines hoarding level based on velocity. -func (t *Tribute) determineHoardingLevel(velocity uint64, cfg *state.TributeConfig) state.HoardingLevel { - if velocity >= cfg.VelocityThresholdMild { - return state.HoardingNone - } - if velocity >= cfg.VelocityThresholdModerate { - return state.HoardingMild - } - if velocity >= cfg.VelocityThresholdSevere { - return state.HoardingModerate - } - if velocity >= cfg.VelocityThresholdExtreme { - return state.HoardingSevere - } - return state.HoardingExtreme -} - -// getTributeRate returns the tribute rate for a hoarding level. -func (t *Tribute) getTributeRate(level state.HoardingLevel, cfg *state.TributeConfig) uint64 { - switch level { - case state.HoardingMild: - return cfg.TributeRateMild - case state.HoardingModerate: - return cfg.TributeRateModerate - case state.HoardingSevere: - return cfg.TributeRateSevere - case state.HoardingExtreme: - return cfg.TributeRateExtreme - default: - return 0 - } -} - -// ===== Contract Methods ===== - -// createVelocityAccount creates a velocity tracking account for a Vita holder. -func (t *Tribute) createVelocityAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - // Verify owner has active Vita - if t.Vita == nil { - panic(ErrTributeNoVita) - } - vita, err := t.Vita.GetTokenByOwner(ic.DAO, owner) - if err != nil || vita == nil { - panic(ErrTributeNoVita) - } - if vita.Status != state.TokenStatusActive { - panic(ErrTributeNoVita) - } - vitaID := vita.TokenID - - // Check if account already exists - existing, _ := t.getAccountInternal(ic.DAO, vitaID) - if existing != nil { - panic(ErrTributeAccountExists) - } - - // Create account - blockHeight := ic.Block.Index - acc := &state.VelocityAccount{ - VitaID: vitaID, - Owner: owner, - CurrentVelocity: 5000, // Default 50% velocity - AverageVelocity: 5000, - LastActivityBlock: blockHeight, - TotalInflow: 0, - TotalOutflow: 0, - StagnantBalance: 0, - HoardingLevel: state.HoardingNone, - ExemptionReason: "", - TotalTributePaid: 0, - TotalIncentivesRcvd: 0, - Status: state.VelocityAccountActive, - CreatedAt: blockHeight, - UpdatedAt: blockHeight, - } - - if err := t.putAccount(ic.DAO, acc); err != nil { - panic(err) - } - t.setOwnerMapping(ic.DAO, owner, vitaID) - - // Update counter - cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) - cache.accountCount++ - t.setAccountCounter(ic.DAO, cache.accountCount) - - // Emit event - ic.AddNotification(t.Hash, VelocityAccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - stackitem.NewByteArray(owner.BytesBE()), - })) - - return stackitem.NewBool(true) -} - -// getAccount returns velocity account by owner. -func (t *Tribute) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.Null{} - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - return stackitem.Null{} - } - - return acc.ToStackItem() -} - -// getAccountByVitaID returns velocity account by Vita ID. -func (t *Tribute) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { - vitaID := toBigInt(args[0]).Uint64() - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - return stackitem.Null{} - } - - return acc.ToStackItem() -} - -// recordTransaction records a transaction for velocity tracking. -func (t *Tribute) recordTransaction(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - amount := toBigInt(args[1]).Uint64() - isOutflow := toBool(args[2]) - - if amount == 0 { - panic(ErrTributeInvalidAmount) - } - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - panic(ErrTributeAccountNotFound) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - panic(ErrTributeAccountNotFound) - } - - if acc.Status == state.VelocityAccountSuspended { - panic(ErrTributeAccountSuspended) - } - - // Update transaction totals - if isOutflow { - acc.TotalOutflow += amount - } else { - acc.TotalInflow += amount - } - - // Recalculate velocity - acc.CurrentVelocity = t.calculateVelocity(acc.TotalInflow, acc.TotalOutflow) - - // Update rolling average (simple average for now) - acc.AverageVelocity = (acc.AverageVelocity + acc.CurrentVelocity) / 2 - - // Determine hoarding level - cfg := t.getConfigInternal(ic.DAO) - acc.HoardingLevel = t.determineHoardingLevel(acc.CurrentVelocity, cfg) - - // Update timestamp - acc.LastActivityBlock = ic.Block.Index - acc.UpdatedAt = ic.Block.Index - - if err := t.putAccount(ic.DAO, acc); err != nil { - panic(err) - } - - // Emit event - ic.AddNotification(t.Hash, VelocityUpdatedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel))), - })) - - return stackitem.NewBool(true) -} - -// getVelocity returns current velocity score for an owner. -func (t *Tribute) getVelocity(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - return stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity)) -} - -// getHoardingLevel returns current hoarding level for an owner. -func (t *Tribute) getHoardingLevel(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - return stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel))) -} - -// grantExemption grants exemption from tribute (admin only). -func (t *Tribute) grantExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - reason := toString(args[1]) - - if !t.checkTributeAdmin(ic) { - panic(ErrTributeNotAdmin) - } - - if len(reason) == 0 || len(reason) > 256 { - panic(ErrTributeInvalidReason) - } - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - panic(ErrTributeAccountNotFound) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - panic(ErrTributeAccountNotFound) - } - - acc.Status = state.VelocityAccountExempt - acc.ExemptionReason = reason - acc.UpdatedAt = ic.Block.Index - - if err := t.putAccount(ic.DAO, acc); err != nil { - panic(err) - } - - // Emit event - ic.AddNotification(t.Hash, ExemptionGrantedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// revokeExemption revokes exemption from tribute (admin only). -func (t *Tribute) revokeExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - if !t.checkTributeAdmin(ic) { - panic(ErrTributeNotAdmin) - } - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - panic(ErrTributeAccountNotFound) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - panic(ErrTributeAccountNotFound) - } - - if acc.Status != state.VelocityAccountExempt { - panic(ErrTributeAccountNotFound) - } - - acc.Status = state.VelocityAccountActive - acc.ExemptionReason = "" - acc.UpdatedAt = ic.Block.Index - - if err := t.putAccount(ic.DAO, acc); err != nil { - panic(err) - } - - // Emit event - ic.AddNotification(t.Hash, ExemptionRevokedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - })) - - return stackitem.NewBool(true) -} - -// isExempt checks if account is exempt from tribute. -func (t *Tribute) isExempt(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBool(false) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - return stackitem.NewBool(false) - } - - return stackitem.NewBool(acc.Status == state.VelocityAccountExempt) -} - -// assessTribute assesses tribute for hoarding. -func (t *Tribute) assessTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - panic(ErrTributeAccountNotFound) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - panic(ErrTributeAccountNotFound) - } - - if acc.Status == state.VelocityAccountExempt { - panic(ErrTributeAccountExempt) - } - - if acc.Status == state.VelocityAccountSuspended { - panic(ErrTributeAccountSuspended) - } - - // Check if there's already a pending assessment - if existingID, exists := t.getPendingAssessmentID(ic.DAO, vitaID); exists { - existing, _ := t.getAssessmentInternal(ic.DAO, existingID) - if existing != nil && existing.Status == state.AssessmentPending { - panic("pending assessment already exists") - } - } - - // Check hoarding level - if acc.HoardingLevel == state.HoardingNone { - panic(ErrTributeNoHoarding) - } - - cfg := t.getConfigInternal(ic.DAO) - - // Get stagnant balance (simplified: use total inflow - outflow as approximation) - stagnantBalance := uint64(0) - if acc.TotalInflow > acc.TotalOutflow { - stagnantBalance = acc.TotalInflow - acc.TotalOutflow - } - - if stagnantBalance < cfg.MinBalanceForTribute { - panic(ErrTributeBelowExemption) - } - - // Calculate tribute - tributeRate := t.getTributeRate(acc.HoardingLevel, cfg) - tributeAmount := (stagnantBalance * tributeRate) / 10000 - - // Create assessment - cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) - assessmentID := cache.assessmentCount - cache.assessmentCount++ - t.setAssessmentCounter(ic.DAO, cache.assessmentCount) - - blockHeight := ic.Block.Index - assessment := &state.TributeAssessment{ - ID: assessmentID, - VitaID: vitaID, - Owner: owner, - AssessmentBlock: blockHeight, - HoardingLevel: acc.HoardingLevel, - StagnantAmount: stagnantBalance, - TributeRate: tributeRate, - TributeAmount: tributeAmount, - DueBlock: blockHeight + cfg.GracePeriod, - CollectedBlock: 0, - Status: state.AssessmentPending, - AppealReason: "", - } - - if err := t.putAssessment(ic.DAO, assessment); err != nil { - panic(err) - } - t.setPendingAssessmentID(ic.DAO, vitaID, assessmentID) - - // Emit event - ic.AddNotification(t.Hash, TributeAssessedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(tributeAmount)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)) -} - -// getAssessment returns assessment by ID. -func (t *Tribute) getAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - assessmentID := toBigInt(args[0]).Uint64() - - assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) - if err != nil || assess == nil { - return stackitem.Null{} - } - - return assess.ToStackItem() -} - -// getPendingAssessment returns pending assessment for an owner. -func (t *Tribute) getPendingAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.Null{} - } - - assessmentID, exists := t.getPendingAssessmentID(ic.DAO, vitaID) - if !exists { - return stackitem.Null{} - } - - assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) - if err != nil || assess == nil { - return stackitem.Null{} - } - - if assess.Status != state.AssessmentPending { - return stackitem.Null{} - } - - return assess.ToStackItem() -} - -// collectTribute collects pending tribute. -func (t *Tribute) collectTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { - assessmentID := toBigInt(args[0]).Uint64() - - assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) - if err != nil || assess == nil { - panic(ErrTributeAssessmentNotFound) - } - - if assess.Status != state.AssessmentPending { - panic(ErrTributeAssessmentNotPending) - } - - // Check property right before taking tribute - if !t.checkPropertyRight(ic, assess.Owner) { - panic(ErrTributePropertyRestricted) - } - - // Mark as collected - assess.Status = state.AssessmentCollected - assess.CollectedBlock = ic.Block.Index - - if err := t.putAssessment(ic.DAO, assess); err != nil { - panic(err) - } - t.deletePendingAssessment(ic.DAO, assess.VitaID) - - // Update account - acc, _ := t.getAccountInternal(ic.DAO, assess.VitaID) - if acc != nil { - acc.TotalTributePaid += assess.TributeAmount - acc.UpdatedAt = ic.Block.Index - t.putAccount(ic.DAO, acc) - } - - // Add to tribute pool for redistribution - pool := t.getTributePoolValue(ic.DAO) - pool += assess.TributeAmount - t.setTributePool(ic.DAO, pool) - - // Emit event - ic.AddNotification(t.Hash, TributeCollectedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(assess.TributeAmount)), - })) - - return stackitem.NewBool(true) -} - -// waiveTribute waives a tribute assessment (admin only). -func (t *Tribute) waiveTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { - assessmentID := toBigInt(args[0]).Uint64() - reason := toString(args[1]) - - if !t.checkTributeAdmin(ic) { - panic(ErrTributeNotAdmin) - } - - if len(reason) == 0 || len(reason) > 256 { - panic(ErrTributeInvalidReason) - } - - assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) - if err != nil || assess == nil { - panic(ErrTributeAssessmentNotFound) - } - - if assess.Status != state.AssessmentPending && assess.Status != state.AssessmentAppealed { - panic(ErrTributeAssessmentNotPending) - } - - assess.Status = state.AssessmentWaived - assess.AppealReason = reason - - if err := t.putAssessment(ic.DAO, assess); err != nil { - panic(err) - } - t.deletePendingAssessment(ic.DAO, assess.VitaID) - - // Emit event - ic.AddNotification(t.Hash, TributeWaivedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// appealTribute appeals a tribute assessment. -func (t *Tribute) appealTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { - assessmentID := toBigInt(args[0]).Uint64() - reason := toString(args[1]) - - if len(reason) == 0 || len(reason) > 512 { - panic(ErrTributeInvalidReason) - } - - assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) - if err != nil || assess == nil { - panic(ErrTributeAssessmentNotFound) - } - - // Check caller is owner - ok, err := checkWitness(ic, assess.Owner) - if err != nil || !ok { - panic(ErrTributeNotOwner) - } - - if assess.Status != state.AssessmentPending { - panic(ErrTributeAssessmentNotPending) - } - - assess.Status = state.AssessmentAppealed - assess.AppealReason = reason - - if err := t.putAssessment(ic.DAO, assess); err != nil { - panic(err) - } - - // Emit event - ic.AddNotification(t.Hash, TributeAppealedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), - stackitem.NewByteArray([]byte(reason)), - })) - - return stackitem.NewBool(true) -} - -// grantIncentive grants circulation incentive (system/admin). -func (t *Tribute) grantIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - incentiveType := state.IncentiveType(toBigInt(args[1]).Uint64()) - amount := toBigInt(args[2]).Uint64() - reason := toString(args[3]) - - if !t.checkTributeAdmin(ic) { - panic(ErrTributeNotAdmin) - } - - if amount == 0 { - panic(ErrTributeInvalidAmount) - } - - if len(reason) == 0 || len(reason) > 256 { - panic(ErrTributeInvalidReason) - } - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - panic(ErrTributeAccountNotFound) - } - - acc, err := t.getAccountInternal(ic.DAO, vitaID) - if err != nil || acc == nil { - panic(ErrTributeAccountNotFound) - } - - // Create incentive - cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) - incentiveID := cache.incentiveCount - cache.incentiveCount++ - t.setIncentiveCounter(ic.DAO, cache.incentiveCount) - - blockHeight := ic.Block.Index - incentive := &state.CirculationIncentive{ - ID: incentiveID, - VitaID: vitaID, - Recipient: owner, - IncentiveType: incentiveType, - Amount: amount, - Reason: reason, - VelocityScore: acc.CurrentVelocity, - GrantedBlock: blockHeight, - ClaimedBlock: 0, - Claimed: false, - } - - if err := t.putIncentive(ic.DAO, incentive); err != nil { - panic(err) - } - t.addUnclaimedIncentive(ic.DAO, vitaID, incentiveID) - - // Emit event - ic.AddNotification(t.Hash, IncentiveGrantedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(amount)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)) -} - -// claimIncentive claims a granted incentive. -func (t *Tribute) claimIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { - incentiveID := toBigInt(args[0]).Uint64() - - incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID) - if err != nil || incentive == nil { - panic(ErrTributeIncentiveNotFound) - } - - // Check caller is recipient - ok, err := checkWitness(ic, incentive.Recipient) - if err != nil || !ok { - panic(ErrTributeNotOwner) - } - - if incentive.Claimed { - panic(ErrTributeIncentiveClaimed) - } - - // Mark as claimed - incentive.Claimed = true - incentive.ClaimedBlock = ic.Block.Index - - if err := t.putIncentive(ic.DAO, incentive); err != nil { - panic(err) - } - t.removeUnclaimedIncentive(ic.DAO, incentive.VitaID, incentiveID) - - // Update account - acc, _ := t.getAccountInternal(ic.DAO, incentive.VitaID) - if acc != nil { - acc.TotalIncentivesRcvd += incentive.Amount - acc.UpdatedAt = ic.Block.Index - t.putAccount(ic.DAO, acc) - } - - // Emit event - ic.AddNotification(t.Hash, IncentiveClaimedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(incentive.Amount)), - })) - - return stackitem.NewBool(true) -} - -// getIncentive returns incentive by ID. -func (t *Tribute) getIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { - incentiveID := toBigInt(args[0]).Uint64() - - incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID) - if err != nil || incentive == nil { - return stackitem.Null{} - } - - return incentive.ToStackItem() -} - -// getUnclaimedIncentives returns count of unclaimed incentives for owner. -func (t *Tribute) getUnclaimedIncentives(ic *interop.Context, args []stackitem.Item) stackitem.Item { - owner := toUint160(args[0]) - - vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) - if !found { - return stackitem.NewBigInteger(big.NewInt(0)) - } - - // Count unclaimed incentives by iterating storage - // This is a simplified version - in production, we'd track this counter - count := uint64(0) - prefix := make([]byte, 9) - prefix[0] = tributePrefixUnclaimedIncentive - binary.BigEndian.PutUint64(prefix[1:], vitaID) - - ic.DAO.Seek(t.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { - count++ - return true - }) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) -} - -// redistribute executes wealth redistribution (committee only). -func (t *Tribute) redistribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { - targetCategory := toString(args[0]) - recipientCount := toBigInt(args[1]).Uint64() - - if !t.checkCommittee(ic) { - panic(ErrTributeNotCommittee) - } - - pool := t.getTributePoolValue(ic.DAO) - if pool == 0 { - panic(ErrTributeNothingToRedistribute) - } - - if recipientCount == 0 { - panic(ErrTributeInvalidAmount) - } - - perCapita := pool / recipientCount - - // Create redistribution record - cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) - redistID := cache.redistributionCount - cache.redistributionCount++ - t.setRedistributionCounter(ic.DAO, cache.redistributionCount) - - record := &state.RedistributionRecord{ - ID: redistID, - SourceAssessment: 0, // Could link to specific assessment if needed - TotalAmount: pool, - RecipientCount: recipientCount, - PerCapitaAmount: perCapita, - RedistBlock: ic.Block.Index, - TargetCategory: targetCategory, - } - - if err := t.putRedistribution(ic.DAO, record); err != nil { - panic(err) - } - - // Clear the pool (actual redistribution would happen via VTS transfers) - t.setTributePool(ic.DAO, 0) - - // Emit event - ic.AddNotification(t.Hash, RedistributionExecutedEvent, stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(redistID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(pool)), - stackitem.NewBigInteger(new(big.Int).SetUint64(recipientCount)), - })) - - return stackitem.NewBigInteger(new(big.Int).SetUint64(redistID)) -} - -// getRedistribution returns redistribution record by ID. -func (t *Tribute) getRedistribution(ic *interop.Context, args []stackitem.Item) stackitem.Item { - redistID := toBigInt(args[0]).Uint64() - - rec, err := t.getRedistributionInternal(ic.DAO, redistID) - if err != nil || rec == nil { - return stackitem.Null{} - } - - return rec.ToStackItem() -} - -// getTributePool returns total tribute pool available for redistribution. -func (t *Tribute) getTributePool(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getTributePoolValue(ic.DAO))) -} - -// getConfig returns current configuration. -func (t *Tribute) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - cfg := t.getConfigInternal(ic.DAO) - if cfg == nil { - return stackitem.Null{} - } - return cfg.ToStackItem() -} - -// getTotalAccounts returns total velocity accounts. -func (t *Tribute) getTotalAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAccountCounter(ic.DAO))) -} - -// getTotalAssessments returns total assessments. -func (t *Tribute) getTotalAssessments(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAssessmentCounter(ic.DAO))) -} - -// getTotalIncentives returns total incentives. -func (t *Tribute) getTotalIncentives(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getIncentiveCounter(ic.DAO))) -} - -// getTotalRedistributions returns total redistributions. -func (t *Tribute) getTotalRedistributions(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getRedistributionCounter(ic.DAO))) -} - -// ===== Public Internal Methods ===== - -// GetAccountByOwner returns velocity account by owner (internal API). -func (t *Tribute) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.VelocityAccount, error) { - vitaID, found := t.getVitaIDByOwner(d, owner) - if !found { - return nil, ErrTributeAccountNotFound - } - return t.getAccountInternal(d, vitaID) -} - -// GetVelocity returns velocity score for an owner (internal API). -func (t *Tribute) GetVelocity(d *dao.Simple, owner util.Uint160) uint64 { - vitaID, found := t.getVitaIDByOwner(d, owner) - if !found { - return 5000 // Default 50% - } - acc, err := t.getAccountInternal(d, vitaID) - if err != nil || acc == nil { - return 5000 - } - return acc.CurrentVelocity -} - -// IsHoarding returns true if owner is hoarding (internal API). -func (t *Tribute) IsHoarding(d *dao.Simple, owner util.Uint160) bool { - vitaID, found := t.getVitaIDByOwner(d, owner) - if !found { - return false - } - acc, err := t.getAccountInternal(d, vitaID) - if err != nil || acc == nil { - return false - } - return acc.HoardingLevel != state.HoardingNone -} - -// Address returns the contract address. -func (t *Tribute) Address() util.Uint160 { - return t.Hash -} +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/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" +) + +// Tribute represents the anti-hoarding economics native contract. +type Tribute struct { + interop.ContractMD + Tutus ITutus + Vita IVita + VTS IVTS + RoleRegistry IRoleRegistry + Lex ILex +} + +// TributeCache represents the cached state for Tribute contract. +type TributeCache struct { + accountCount uint64 + assessmentCount uint64 + incentiveCount uint64 + redistributionCount uint64 +} + +// Storage key prefixes for Tribute. +const ( + tributePrefixAccount byte = 0x01 // vitaID -> VelocityAccount + tributePrefixAccountByOwner byte = 0x02 // owner -> vitaID + tributePrefixAssessment byte = 0x10 // assessmentID -> TributeAssessment + tributePrefixAssessmentByOwner byte = 0x11 // vitaID + assessmentID -> exists + tributePrefixPendingAssessment byte = 0x12 // vitaID -> latest pending assessmentID + tributePrefixIncentive byte = 0x20 // incentiveID -> CirculationIncentive + tributePrefixIncentiveByOwner byte = 0x21 // vitaID + incentiveID -> exists + tributePrefixUnclaimedIncentive byte = 0x22 // vitaID + incentiveID -> exists (unclaimed only) + tributePrefixRedistribution byte = 0x30 // redistID -> RedistributionRecord + tributePrefixAccountCounter byte = 0xF0 // -> uint64 + tributePrefixAssessmentCounter byte = 0xF1 // -> next assessment ID + tributePrefixIncentiveCounter byte = 0xF2 // -> next incentive ID + tributePrefixRedistributionCtr byte = 0xF3 // -> next redistribution ID + tributePrefixTotalTributePool byte = 0xF8 // -> total tribute collected for redistribution + tributePrefixConfig byte = 0xFF // -> TributeConfig +) + +// Event names for Tribute. +const ( + VelocityAccountCreatedEvent = "VelocityAccountCreated" + VelocityUpdatedEvent = "VelocityUpdated" + TributeAssessedEvent = "TributeAssessed" + TributeCollectedEvent = "TributeCollected" + TributeWaivedEvent = "TributeWaived" + TributeAppealedEvent = "TributeAppealed" + IncentiveGrantedEvent = "IncentiveGranted" + IncentiveClaimedEvent = "IncentiveClaimed" + RedistributionExecutedEvent = "RedistributionExecuted" + ExemptionGrantedEvent = "ExemptionGranted" + ExemptionRevokedEvent = "ExemptionRevoked" +) + +// Role constants for tribute administrators. +const ( + RoleTributeAdmin uint64 = 23 // Can manage exemptions and appeals +) + +// Various errors for Tribute. +var ( + ErrTributeAccountNotFound = errors.New("velocity account not found") + ErrTributeAccountExists = errors.New("velocity account already exists") + ErrTributeAccountExempt = errors.New("account is exempt from tribute") + ErrTributeAccountSuspended = errors.New("velocity account is suspended") + ErrTributeNoVita = errors.New("owner must have an active Vita") + ErrTributeAssessmentNotFound = errors.New("tribute assessment not found") + ErrTributeAssessmentNotPending = errors.New("assessment is not pending") + ErrTributeAssessmentAlreadyPaid = errors.New("assessment already collected") + ErrTributeIncentiveNotFound = errors.New("incentive not found") + ErrTributeIncentiveClaimed = errors.New("incentive already claimed") + ErrTributeInsufficientBalance = errors.New("insufficient balance for tribute") + ErrTributeNotCommittee = errors.New("invalid committee signature") + ErrTributeNotOwner = errors.New("caller is not the owner") + ErrTributeNotAdmin = errors.New("caller is not an authorized tribute admin") + ErrTributePropertyRestricted = errors.New("property right is restricted") + ErrTributeInvalidAmount = errors.New("invalid amount") + ErrTributeInvalidReason = errors.New("invalid reason") + ErrTributeBelowExemption = errors.New("balance below exemption threshold") + ErrTributeNoHoarding = errors.New("no hoarding detected") + ErrTributeNothingToRedistribute = errors.New("nothing to redistribute") +) + +var ( + _ interop.Contract = (*Tribute)(nil) + _ dao.NativeContractCache = (*TributeCache)(nil) +) + +// Copy implements NativeContractCache interface. +func (c *TributeCache) Copy() dao.NativeContractCache { + return &TributeCache{ + accountCount: c.accountCount, + assessmentCount: c.assessmentCount, + incentiveCount: c.incentiveCount, + redistributionCount: c.redistributionCount, + } +} + +// checkCommittee checks if the caller has committee authority. +func (t *Tribute) checkCommittee(ic *interop.Context) bool { + if t.RoleRegistry != nil { + return t.RoleRegistry.CheckCommittee(ic) + } + return t.Tutus.CheckCommittee(ic) +} + +// checkTributeAdmin checks if the caller has tribute admin authority. +func (t *Tribute) checkTributeAdmin(ic *interop.Context) bool { + caller := ic.VM.GetCallingScriptHash() + if t.RoleRegistry != nil { + if t.RoleRegistry.HasRoleInternal(ic.DAO, caller, RoleTributeAdmin, ic.Block.Index) { + return true + } + } + // Committee members can also act as tribute admins + return t.checkCommittee(ic) +} + +// checkPropertyRight checks if subject has property rights via Lex. +func (t *Tribute) checkPropertyRight(ic *interop.Context, subject util.Uint160) bool { + if t.Lex == nil { + return true // Allow if Lex not available + } + return t.Lex.HasRightInternal(ic.DAO, subject, state.RightProperty, ic.Block.Index) +} + +// newTribute creates a new Tribute native contract. +func newTribute() *Tribute { + t := &Tribute{ + ContractMD: *interop.NewContractMD(nativenames.Tribute, nativeids.Tribute), + } + defer t.BuildHFSpecificMD(t.ActiveIn()) + + // ===== Account Management ===== + + // createVelocityAccount - Create velocity tracking account for a Vita holder + desc := NewDescriptor("createVelocityAccount", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md := NewMethodAndPrice(t.createVelocityAccount, 1<<17, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // getAccount - Get velocity account by owner + desc = NewDescriptor("getAccount", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.getAccount, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getAccountByVitaID - Get account by Vita ID + desc = NewDescriptor("getAccountByVitaID", smartcontract.ArrayType, + manifest.NewParameter("vitaID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.getAccountByVitaID, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // recordTransaction - Record a transaction for velocity tracking + desc = NewDescriptor("recordTransaction", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("isOutflow", smartcontract.BoolType)) + md = NewMethodAndPrice(t.recordTransaction, 1<<16, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // getVelocity - Get current velocity score for an owner + desc = NewDescriptor("getVelocity", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.getVelocity, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getHoardingLevel - Get current hoarding level for an owner + desc = NewDescriptor("getHoardingLevel", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.getHoardingLevel, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // ===== Exemption Management ===== + + // grantExemption - Grant exemption from tribute (admin only) + desc = NewDescriptor("grantExemption", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(t.grantExemption, 1<<16, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // revokeExemption - Revoke exemption from tribute (admin only) + desc = NewDescriptor("revokeExemption", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.revokeExemption, 1<<16, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // isExempt - Check if account is exempt + desc = NewDescriptor("isExempt", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.isExempt, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // ===== Assessment Management ===== + + // assessTribute - Assess tribute for hoarding (called periodically or on-demand) + desc = NewDescriptor("assessTribute", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.assessTribute, 1<<17, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // getAssessment - Get assessment by ID + desc = NewDescriptor("getAssessment", smartcontract.ArrayType, + manifest.NewParameter("assessmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.getAssessment, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getPendingAssessment - Get pending assessment for an owner + desc = NewDescriptor("getPendingAssessment", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.getPendingAssessment, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // collectTribute - Collect pending tribute + desc = NewDescriptor("collectTribute", smartcontract.BoolType, + manifest.NewParameter("assessmentID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.collectTribute, 1<<17, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // waiveTribute - Waive a tribute assessment (admin only) + desc = NewDescriptor("waiveTribute", smartcontract.BoolType, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(t.waiveTribute, 1<<16, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // appealTribute - Appeal a tribute assessment + desc = NewDescriptor("appealTribute", smartcontract.BoolType, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(t.appealTribute, 1<<16, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // ===== Incentive Management ===== + + // grantIncentive - Grant circulation incentive (system/admin) + desc = NewDescriptor("grantIncentive", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("incentiveType", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(t.grantIncentive, 1<<17, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // claimIncentive - Claim a granted incentive + desc = NewDescriptor("claimIncentive", smartcontract.BoolType, + manifest.NewParameter("incentiveID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.claimIncentive, 1<<17, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // getIncentive - Get incentive by ID + desc = NewDescriptor("getIncentive", smartcontract.ArrayType, + manifest.NewParameter("incentiveID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.getIncentive, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getUnclaimedIncentives - Get count of unclaimed incentives for owner + desc = NewDescriptor("getUnclaimedIncentives", smartcontract.IntegerType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(t.getUnclaimedIncentives, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // ===== Redistribution ===== + + // redistribute - Execute wealth redistribution (committee only) + desc = NewDescriptor("redistribute", smartcontract.IntegerType, + manifest.NewParameter("targetCategory", smartcontract.StringType), + manifest.NewParameter("recipientCount", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.redistribute, 1<<18, callflag.States|callflag.AllowNotify) + t.AddMethod(md, desc) + + // getRedistribution - Get redistribution record by ID + desc = NewDescriptor("getRedistribution", smartcontract.ArrayType, + manifest.NewParameter("redistID", smartcontract.IntegerType)) + md = NewMethodAndPrice(t.getRedistribution, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getTributePool - Get total tribute pool available for redistribution + desc = NewDescriptor("getTributePool", smartcontract.IntegerType) + md = NewMethodAndPrice(t.getTributePool, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // ===== Configuration & Stats ===== + + // getConfig - Get current configuration + desc = NewDescriptor("getConfig", smartcontract.ArrayType) + md = NewMethodAndPrice(t.getConfig, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getTotalAccounts - Get total velocity accounts + desc = NewDescriptor("getTotalAccounts", smartcontract.IntegerType) + md = NewMethodAndPrice(t.getTotalAccounts, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getTotalAssessments - Get total assessments + desc = NewDescriptor("getTotalAssessments", smartcontract.IntegerType) + md = NewMethodAndPrice(t.getTotalAssessments, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getTotalIncentives - Get total incentives + desc = NewDescriptor("getTotalIncentives", smartcontract.IntegerType) + md = NewMethodAndPrice(t.getTotalIncentives, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // getTotalRedistributions - Get total redistributions + desc = NewDescriptor("getTotalRedistributions", smartcontract.IntegerType) + md = NewMethodAndPrice(t.getTotalRedistributions, 1<<15, callflag.ReadStates) + t.AddMethod(md, desc) + + // ===== Events ===== + + // VelocityAccountCreated event + eDesc := NewEventDescriptor(VelocityAccountCreatedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("owner", smartcontract.Hash160Type)) + t.AddEvent(NewEvent(eDesc)) + + // VelocityUpdated event + eDesc = NewEventDescriptor(VelocityUpdatedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("newVelocity", smartcontract.IntegerType), + manifest.NewParameter("hoardingLevel", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // TributeAssessed event + eDesc = NewEventDescriptor(TributeAssessedEvent, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // TributeCollected event + eDesc = NewEventDescriptor(TributeCollectedEvent, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // TributeWaived event + eDesc = NewEventDescriptor(TributeWaivedEvent, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + t.AddEvent(NewEvent(eDesc)) + + // TributeAppealed event + eDesc = NewEventDescriptor(TributeAppealedEvent, + manifest.NewParameter("assessmentID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + t.AddEvent(NewEvent(eDesc)) + + // IncentiveGranted event + eDesc = NewEventDescriptor(IncentiveGrantedEvent, + manifest.NewParameter("incentiveID", smartcontract.IntegerType), + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // IncentiveClaimed event + eDesc = NewEventDescriptor(IncentiveClaimedEvent, + manifest.NewParameter("incentiveID", smartcontract.IntegerType), + manifest.NewParameter("amount", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // RedistributionExecuted event + eDesc = NewEventDescriptor(RedistributionExecutedEvent, + manifest.NewParameter("redistID", smartcontract.IntegerType), + manifest.NewParameter("totalAmount", smartcontract.IntegerType), + manifest.NewParameter("recipientCount", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + // ExemptionGranted event + eDesc = NewEventDescriptor(ExemptionGrantedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + t.AddEvent(NewEvent(eDesc)) + + // ExemptionRevoked event + eDesc = NewEventDescriptor(ExemptionRevokedEvent, + manifest.NewParameter("vitaID", smartcontract.IntegerType)) + t.AddEvent(NewEvent(eDesc)) + + return t +} + +// Metadata returns contract metadata. +func (t *Tribute) Metadata() *interop.ContractMD { + return &t.ContractMD +} + +// Initialize initializes the Tribute contract. +func (t *Tribute) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error { + if hf != t.ActiveIn() { + return nil + } + + // Initialize counters + t.setAccountCounter(ic.DAO, 0) + t.setAssessmentCounter(ic.DAO, 0) + t.setIncentiveCounter(ic.DAO, 0) + t.setRedistributionCounter(ic.DAO, 0) + t.setTributePool(ic.DAO, 0) + + // Initialize config with defaults + // Velocity in basis points (0-10000 = 0%-100%) + cfg := &state.TributeConfig{ + VelocityThresholdMild: 5000, // Below 50% velocity = mild hoarding + VelocityThresholdModerate: 3000, // Below 30% = moderate hoarding + VelocityThresholdSevere: 1500, // Below 15% = severe hoarding + VelocityThresholdExtreme: 500, // Below 5% = extreme hoarding + TributeRateMild: 100, // 1% tribute for mild hoarding + TributeRateModerate: 300, // 3% tribute for moderate hoarding + TributeRateSevere: 700, // 7% tribute for severe hoarding + TributeRateExtreme: 1500, // 15% tribute for extreme hoarding + IncentiveRateHigh: 50, // 0.5% incentive for high velocity + IncentiveRateVeryHigh: 150, // 1.5% incentive for very high velocity + StagnancyPeriod: 86400, // ~1 day (1-second blocks) before balance is stagnant + AssessmentPeriod: 604800, // ~7 days between assessments + GracePeriod: 259200, // ~3 days to pay tribute + MinBalanceForTribute: 1000000, // 1 VTS minimum to assess + ExemptionThreshold: 100000, // 0.1 VTS exempt + } + t.setConfig(ic.DAO, cfg) + + // Initialize cache + cache := &TributeCache{ + accountCount: 0, + assessmentCount: 0, + incentiveCount: 0, + redistributionCount: 0, + } + ic.DAO.SetCache(t.ID, cache) + + return nil +} + +// InitializeCache initializes the cache from storage. +func (t *Tribute) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error { + cache := &TributeCache{ + accountCount: t.getAccountCounter(d), + assessmentCount: t.getAssessmentCounter(d), + incentiveCount: t.getIncentiveCounter(d), + redistributionCount: t.getRedistributionCounter(d), + } + d.SetCache(t.ID, cache) + return nil +} + +// OnPersist is called before block is committed. +func (t *Tribute) OnPersist(ic *interop.Context) error { + return nil +} + +// PostPersist is called after block is committed. +func (t *Tribute) PostPersist(ic *interop.Context) error { + return nil +} + +// ActiveIn returns the hardfork at which this contract is activated. +func (t *Tribute) ActiveIn() *config.Hardfork { + return nil // Always active +} + +// ===== Storage Helpers ===== + +func (t *Tribute) makeAccountKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = tributePrefixAccount + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func (t *Tribute) makeAccountByOwnerKey(owner util.Uint160) []byte { + key := make([]byte, 21) + key[0] = tributePrefixAccountByOwner + copy(key[1:], owner.BytesBE()) + return key +} + +func (t *Tribute) makeAssessmentKey(assessmentID uint64) []byte { + key := make([]byte, 9) + key[0] = tributePrefixAssessment + binary.BigEndian.PutUint64(key[1:], assessmentID) + return key +} + +func (t *Tribute) makePendingAssessmentKey(vitaID uint64) []byte { + key := make([]byte, 9) + key[0] = tributePrefixPendingAssessment + binary.BigEndian.PutUint64(key[1:], vitaID) + return key +} + +func (t *Tribute) makeIncentiveKey(incentiveID uint64) []byte { + key := make([]byte, 9) + key[0] = tributePrefixIncentive + binary.BigEndian.PutUint64(key[1:], incentiveID) + return key +} + +func (t *Tribute) makeUnclaimedIncentiveKey(vitaID, incentiveID uint64) []byte { + key := make([]byte, 17) + key[0] = tributePrefixUnclaimedIncentive + binary.BigEndian.PutUint64(key[1:], vitaID) + binary.BigEndian.PutUint64(key[9:], incentiveID) + return key +} + +func (t *Tribute) makeRedistributionKey(redistID uint64) []byte { + key := make([]byte, 9) + key[0] = tributePrefixRedistribution + binary.BigEndian.PutUint64(key[1:], redistID) + return key +} + +// ===== Counter Helpers ===== + +func (t *Tribute) getAccountCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(t.ID, []byte{tributePrefixAccountCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (t *Tribute) setAccountCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(t.ID, []byte{tributePrefixAccountCounter}, buf) +} + +func (t *Tribute) getAssessmentCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(t.ID, []byte{tributePrefixAssessmentCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (t *Tribute) setAssessmentCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(t.ID, []byte{tributePrefixAssessmentCounter}, buf) +} + +func (t *Tribute) getIncentiveCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(t.ID, []byte{tributePrefixIncentiveCounter}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (t *Tribute) setIncentiveCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(t.ID, []byte{tributePrefixIncentiveCounter}, buf) +} + +func (t *Tribute) getRedistributionCounter(d *dao.Simple) uint64 { + si := d.GetStorageItem(t.ID, []byte{tributePrefixRedistributionCtr}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (t *Tribute) setRedistributionCounter(d *dao.Simple, count uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, count) + d.PutStorageItem(t.ID, []byte{tributePrefixRedistributionCtr}, buf) +} + +func (t *Tribute) getTributePoolValue(d *dao.Simple) uint64 { + si := d.GetStorageItem(t.ID, []byte{tributePrefixTotalTributePool}) + if si == nil { + return 0 + } + return binary.BigEndian.Uint64(si) +} + +func (t *Tribute) setTributePool(d *dao.Simple, amount uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, amount) + d.PutStorageItem(t.ID, []byte{tributePrefixTotalTributePool}, buf) +} + +// ===== Account Storage ===== + +func (t *Tribute) getAccountInternal(d *dao.Simple, vitaID uint64) (*state.VelocityAccount, error) { + si := d.GetStorageItem(t.ID, t.makeAccountKey(vitaID)) + if si == nil { + return nil, nil + } + acc := new(state.VelocityAccount) + item, err := stackitem.Deserialize(si) + if err != nil { + return nil, err + } + if err := acc.FromStackItem(item); err != nil { + return nil, err + } + return acc, nil +} + +func (t *Tribute) putAccount(d *dao.Simple, acc *state.VelocityAccount) error { + data, err := stackitem.Serialize(acc.ToStackItem()) + if err != nil { + return err + } + d.PutStorageItem(t.ID, t.makeAccountKey(acc.VitaID), data) + return nil +} + +func (t *Tribute) getVitaIDByOwner(d *dao.Simple, owner util.Uint160) (uint64, bool) { + si := d.GetStorageItem(t.ID, t.makeAccountByOwnerKey(owner)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (t *Tribute) setOwnerMapping(d *dao.Simple, owner util.Uint160, vitaID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, vitaID) + d.PutStorageItem(t.ID, t.makeAccountByOwnerKey(owner), buf) +} + +// ===== Assessment Storage ===== + +func (t *Tribute) getAssessmentInternal(d *dao.Simple, assessmentID uint64) (*state.TributeAssessment, error) { + si := d.GetStorageItem(t.ID, t.makeAssessmentKey(assessmentID)) + if si == nil { + return nil, nil + } + assess := new(state.TributeAssessment) + item, err := stackitem.Deserialize(si) + if err != nil { + return nil, err + } + if err := assess.FromStackItem(item); err != nil { + return nil, err + } + return assess, nil +} + +func (t *Tribute) putAssessment(d *dao.Simple, assess *state.TributeAssessment) error { + data, err := stackitem.Serialize(assess.ToStackItem()) + if err != nil { + return err + } + d.PutStorageItem(t.ID, t.makeAssessmentKey(assess.ID), data) + return nil +} + +func (t *Tribute) getPendingAssessmentID(d *dao.Simple, vitaID uint64) (uint64, bool) { + si := d.GetStorageItem(t.ID, t.makePendingAssessmentKey(vitaID)) + if si == nil { + return 0, false + } + return binary.BigEndian.Uint64(si), true +} + +func (t *Tribute) setPendingAssessmentID(d *dao.Simple, vitaID, assessmentID uint64) { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, assessmentID) + d.PutStorageItem(t.ID, t.makePendingAssessmentKey(vitaID), buf) +} + +func (t *Tribute) deletePendingAssessment(d *dao.Simple, vitaID uint64) { + d.DeleteStorageItem(t.ID, t.makePendingAssessmentKey(vitaID)) +} + +// ===== Incentive Storage ===== + +func (t *Tribute) getIncentiveInternal(d *dao.Simple, incentiveID uint64) (*state.CirculationIncentive, error) { + si := d.GetStorageItem(t.ID, t.makeIncentiveKey(incentiveID)) + if si == nil { + return nil, nil + } + inc := new(state.CirculationIncentive) + item, err := stackitem.Deserialize(si) + if err != nil { + return nil, err + } + if err := inc.FromStackItem(item); err != nil { + return nil, err + } + return inc, nil +} + +func (t *Tribute) putIncentive(d *dao.Simple, inc *state.CirculationIncentive) error { + data, err := stackitem.Serialize(inc.ToStackItem()) + if err != nil { + return err + } + d.PutStorageItem(t.ID, t.makeIncentiveKey(inc.ID), data) + return nil +} + +func (t *Tribute) addUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) { + d.PutStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID), []byte{1}) +} + +func (t *Tribute) removeUnclaimedIncentive(d *dao.Simple, vitaID, incentiveID uint64) { + d.DeleteStorageItem(t.ID, t.makeUnclaimedIncentiveKey(vitaID, incentiveID)) +} + +// ===== Redistribution Storage ===== + +func (t *Tribute) getRedistributionInternal(d *dao.Simple, redistID uint64) (*state.RedistributionRecord, error) { + si := d.GetStorageItem(t.ID, t.makeRedistributionKey(redistID)) + if si == nil { + return nil, nil + } + rec := new(state.RedistributionRecord) + item, err := stackitem.Deserialize(si) + if err != nil { + return nil, err + } + if err := rec.FromStackItem(item); err != nil { + return nil, err + } + return rec, nil +} + +func (t *Tribute) putRedistribution(d *dao.Simple, rec *state.RedistributionRecord) error { + data, err := stackitem.Serialize(rec.ToStackItem()) + if err != nil { + return err + } + d.PutStorageItem(t.ID, t.makeRedistributionKey(rec.ID), data) + return nil +} + +// ===== Config Storage ===== + +func (t *Tribute) getConfigInternal(d *dao.Simple) *state.TributeConfig { + si := d.GetStorageItem(t.ID, []byte{tributePrefixConfig}) + if si == nil { + return nil + } + cfg := new(state.TributeConfig) + item, err := stackitem.Deserialize(si) + if err != nil { + return nil + } + if err := cfg.FromStackItem(item); err != nil { + return nil + } + return cfg +} + +func (t *Tribute) setConfig(d *dao.Simple, cfg *state.TributeConfig) { + data, _ := stackitem.Serialize(cfg.ToStackItem()) + d.PutStorageItem(t.ID, []byte{tributePrefixConfig}, data) +} + +// ===== Internal Helpers ===== + +// calculateVelocity calculates velocity based on inflow/outflow ratio. +func (t *Tribute) calculateVelocity(totalInflow, totalOutflow uint64) uint64 { + if totalInflow == 0 { + if totalOutflow > 0 { + return 10000 // 100% velocity if only outflow + } + return 5000 // Default 50% if no activity + } + // Velocity = (outflow / inflow) * 10000 (basis points) + velocity := (totalOutflow * 10000) / totalInflow + if velocity > 10000 { + velocity = 10000 + } + return velocity +} + +// determineHoardingLevel determines hoarding level based on velocity. +func (t *Tribute) determineHoardingLevel(velocity uint64, cfg *state.TributeConfig) state.HoardingLevel { + if velocity >= cfg.VelocityThresholdMild { + return state.HoardingNone + } + if velocity >= cfg.VelocityThresholdModerate { + return state.HoardingMild + } + if velocity >= cfg.VelocityThresholdSevere { + return state.HoardingModerate + } + if velocity >= cfg.VelocityThresholdExtreme { + return state.HoardingSevere + } + return state.HoardingExtreme +} + +// getTributeRate returns the tribute rate for a hoarding level. +func (t *Tribute) getTributeRate(level state.HoardingLevel, cfg *state.TributeConfig) uint64 { + switch level { + case state.HoardingMild: + return cfg.TributeRateMild + case state.HoardingModerate: + return cfg.TributeRateModerate + case state.HoardingSevere: + return cfg.TributeRateSevere + case state.HoardingExtreme: + return cfg.TributeRateExtreme + default: + return 0 + } +} + +// ===== Contract Methods ===== + +// createVelocityAccount creates a velocity tracking account for a Vita holder. +func (t *Tribute) createVelocityAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + // Verify owner has active Vita + if t.Vita == nil { + panic(ErrTributeNoVita) + } + vita, err := t.Vita.GetTokenByOwner(ic.DAO, owner) + if err != nil || vita == nil { + panic(ErrTributeNoVita) + } + if vita.Status != state.TokenStatusActive { + panic(ErrTributeNoVita) + } + vitaID := vita.TokenID + + // Check if account already exists + existing, _ := t.getAccountInternal(ic.DAO, vitaID) + if existing != nil { + panic(ErrTributeAccountExists) + } + + // Create account + blockHeight := ic.Block.Index + acc := &state.VelocityAccount{ + VitaID: vitaID, + Owner: owner, + CurrentVelocity: 5000, // Default 50% velocity + AverageVelocity: 5000, + LastActivityBlock: blockHeight, + TotalInflow: 0, + TotalOutflow: 0, + StagnantBalance: 0, + HoardingLevel: state.HoardingNone, + ExemptionReason: "", + TotalTributePaid: 0, + TotalIncentivesRcvd: 0, + Status: state.VelocityAccountActive, + CreatedAt: blockHeight, + UpdatedAt: blockHeight, + } + + if err := t.putAccount(ic.DAO, acc); err != nil { + panic(err) + } + t.setOwnerMapping(ic.DAO, owner, vitaID) + + // Update counter + cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) + cache.accountCount++ + t.setAccountCounter(ic.DAO, cache.accountCount) + + // Emit event + ic.AddNotification(t.Hash, VelocityAccountCreatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + stackitem.NewByteArray(owner.BytesBE()), + })) + + return stackitem.NewBool(true) +} + +// getAccount returns velocity account by owner. +func (t *Tribute) getAccount(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.Null{} + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + return stackitem.Null{} + } + + return acc.ToStackItem() +} + +// getAccountByVitaID returns velocity account by Vita ID. +func (t *Tribute) getAccountByVitaID(ic *interop.Context, args []stackitem.Item) stackitem.Item { + vitaID := toBigInt(args[0]).Uint64() + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + return stackitem.Null{} + } + + return acc.ToStackItem() +} + +// recordTransaction records a transaction for velocity tracking. +func (t *Tribute) recordTransaction(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + amount := toBigInt(args[1]).Uint64() + isOutflow := toBool(args[2]) + + if amount == 0 { + panic(ErrTributeInvalidAmount) + } + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + panic(ErrTributeAccountNotFound) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + panic(ErrTributeAccountNotFound) + } + + if acc.Status == state.VelocityAccountSuspended { + panic(ErrTributeAccountSuspended) + } + + // Update transaction totals + if isOutflow { + acc.TotalOutflow += amount + } else { + acc.TotalInflow += amount + } + + // Recalculate velocity + acc.CurrentVelocity = t.calculateVelocity(acc.TotalInflow, acc.TotalOutflow) + + // Update rolling average (simple average for now) + acc.AverageVelocity = (acc.AverageVelocity + acc.CurrentVelocity) / 2 + + // Determine hoarding level + cfg := t.getConfigInternal(ic.DAO) + acc.HoardingLevel = t.determineHoardingLevel(acc.CurrentVelocity, cfg) + + // Update timestamp + acc.LastActivityBlock = ic.Block.Index + acc.UpdatedAt = ic.Block.Index + + if err := t.putAccount(ic.DAO, acc); err != nil { + panic(err) + } + + // Emit event + ic.AddNotification(t.Hash, VelocityUpdatedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel))), + })) + + return stackitem.NewBool(true) +} + +// getVelocity returns current velocity score for an owner. +func (t *Tribute) getVelocity(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + return stackitem.NewBigInteger(new(big.Int).SetUint64(acc.CurrentVelocity)) +} + +// getHoardingLevel returns current hoarding level for an owner. +func (t *Tribute) getHoardingLevel(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + return stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(acc.HoardingLevel))) +} + +// grantExemption grants exemption from tribute (admin only). +func (t *Tribute) grantExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + reason := toString(args[1]) + + if !t.checkTributeAdmin(ic) { + panic(ErrTributeNotAdmin) + } + + if len(reason) == 0 || len(reason) > 256 { + panic(ErrTributeInvalidReason) + } + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + panic(ErrTributeAccountNotFound) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + panic(ErrTributeAccountNotFound) + } + + acc.Status = state.VelocityAccountExempt + acc.ExemptionReason = reason + acc.UpdatedAt = ic.Block.Index + + if err := t.putAccount(ic.DAO, acc); err != nil { + panic(err) + } + + // Emit event + ic.AddNotification(t.Hash, ExemptionGrantedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// revokeExemption revokes exemption from tribute (admin only). +func (t *Tribute) revokeExemption(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + if !t.checkTributeAdmin(ic) { + panic(ErrTributeNotAdmin) + } + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + panic(ErrTributeAccountNotFound) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + panic(ErrTributeAccountNotFound) + } + + if acc.Status != state.VelocityAccountExempt { + panic(ErrTributeAccountNotFound) + } + + acc.Status = state.VelocityAccountActive + acc.ExemptionReason = "" + acc.UpdatedAt = ic.Block.Index + + if err := t.putAccount(ic.DAO, acc); err != nil { + panic(err) + } + + // Emit event + ic.AddNotification(t.Hash, ExemptionRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + })) + + return stackitem.NewBool(true) +} + +// isExempt checks if account is exempt from tribute. +func (t *Tribute) isExempt(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBool(false) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + return stackitem.NewBool(false) + } + + return stackitem.NewBool(acc.Status == state.VelocityAccountExempt) +} + +// assessTribute assesses tribute for hoarding. +func (t *Tribute) assessTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + panic(ErrTributeAccountNotFound) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + panic(ErrTributeAccountNotFound) + } + + if acc.Status == state.VelocityAccountExempt { + panic(ErrTributeAccountExempt) + } + + if acc.Status == state.VelocityAccountSuspended { + panic(ErrTributeAccountSuspended) + } + + // Check if there's already a pending assessment + if existingID, exists := t.getPendingAssessmentID(ic.DAO, vitaID); exists { + existing, _ := t.getAssessmentInternal(ic.DAO, existingID) + if existing != nil && existing.Status == state.AssessmentPending { + panic("pending assessment already exists") + } + } + + // Check hoarding level + if acc.HoardingLevel == state.HoardingNone { + panic(ErrTributeNoHoarding) + } + + cfg := t.getConfigInternal(ic.DAO) + + // Get stagnant balance (simplified: use total inflow - outflow as approximation) + stagnantBalance := uint64(0) + if acc.TotalInflow > acc.TotalOutflow { + stagnantBalance = acc.TotalInflow - acc.TotalOutflow + } + + if stagnantBalance < cfg.MinBalanceForTribute { + panic(ErrTributeBelowExemption) + } + + // Calculate tribute + tributeRate := t.getTributeRate(acc.HoardingLevel, cfg) + tributeAmount := (stagnantBalance * tributeRate) / 10000 + + // Create assessment + cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) + assessmentID := cache.assessmentCount + cache.assessmentCount++ + t.setAssessmentCounter(ic.DAO, cache.assessmentCount) + + blockHeight := ic.Block.Index + assessment := &state.TributeAssessment{ + ID: assessmentID, + VitaID: vitaID, + Owner: owner, + AssessmentBlock: blockHeight, + HoardingLevel: acc.HoardingLevel, + StagnantAmount: stagnantBalance, + TributeRate: tributeRate, + TributeAmount: tributeAmount, + DueBlock: blockHeight + cfg.GracePeriod, + CollectedBlock: 0, + Status: state.AssessmentPending, + AppealReason: "", + } + + if err := t.putAssessment(ic.DAO, assessment); err != nil { + panic(err) + } + t.setPendingAssessmentID(ic.DAO, vitaID, assessmentID) + + // Emit event + ic.AddNotification(t.Hash, TributeAssessedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(tributeAmount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)) +} + +// getAssessment returns assessment by ID. +func (t *Tribute) getAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + assessmentID := toBigInt(args[0]).Uint64() + + assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) + if err != nil || assess == nil { + return stackitem.Null{} + } + + return assess.ToStackItem() +} + +// getPendingAssessment returns pending assessment for an owner. +func (t *Tribute) getPendingAssessment(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.Null{} + } + + assessmentID, exists := t.getPendingAssessmentID(ic.DAO, vitaID) + if !exists { + return stackitem.Null{} + } + + assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) + if err != nil || assess == nil { + return stackitem.Null{} + } + + if assess.Status != state.AssessmentPending { + return stackitem.Null{} + } + + return assess.ToStackItem() +} + +// collectTribute collects pending tribute. +func (t *Tribute) collectTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { + assessmentID := toBigInt(args[0]).Uint64() + + assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) + if err != nil || assess == nil { + panic(ErrTributeAssessmentNotFound) + } + + if assess.Status != state.AssessmentPending { + panic(ErrTributeAssessmentNotPending) + } + + // Check property right before taking tribute + if !t.checkPropertyRight(ic, assess.Owner) { + panic(ErrTributePropertyRestricted) + } + + // Mark as collected + assess.Status = state.AssessmentCollected + assess.CollectedBlock = ic.Block.Index + + if err := t.putAssessment(ic.DAO, assess); err != nil { + panic(err) + } + t.deletePendingAssessment(ic.DAO, assess.VitaID) + + // Update account + acc, _ := t.getAccountInternal(ic.DAO, assess.VitaID) + if acc != nil { + acc.TotalTributePaid += assess.TributeAmount + acc.UpdatedAt = ic.Block.Index + t.putAccount(ic.DAO, acc) + } + + // Add to tribute pool for redistribution + pool := t.getTributePoolValue(ic.DAO) + pool += assess.TributeAmount + t.setTributePool(ic.DAO, pool) + + // Emit event + ic.AddNotification(t.Hash, TributeCollectedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(assess.TributeAmount)), + })) + + return stackitem.NewBool(true) +} + +// waiveTribute waives a tribute assessment (admin only). +func (t *Tribute) waiveTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { + assessmentID := toBigInt(args[0]).Uint64() + reason := toString(args[1]) + + if !t.checkTributeAdmin(ic) { + panic(ErrTributeNotAdmin) + } + + if len(reason) == 0 || len(reason) > 256 { + panic(ErrTributeInvalidReason) + } + + assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) + if err != nil || assess == nil { + panic(ErrTributeAssessmentNotFound) + } + + if assess.Status != state.AssessmentPending && assess.Status != state.AssessmentAppealed { + panic(ErrTributeAssessmentNotPending) + } + + assess.Status = state.AssessmentWaived + assess.AppealReason = reason + + if err := t.putAssessment(ic.DAO, assess); err != nil { + panic(err) + } + t.deletePendingAssessment(ic.DAO, assess.VitaID) + + // Emit event + ic.AddNotification(t.Hash, TributeWaivedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// appealTribute appeals a tribute assessment. +func (t *Tribute) appealTribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { + assessmentID := toBigInt(args[0]).Uint64() + reason := toString(args[1]) + + if len(reason) == 0 || len(reason) > 512 { + panic(ErrTributeInvalidReason) + } + + assess, err := t.getAssessmentInternal(ic.DAO, assessmentID) + if err != nil || assess == nil { + panic(ErrTributeAssessmentNotFound) + } + + // Check caller is owner + ok, err := checkWitness(ic, assess.Owner) + if err != nil || !ok { + panic(ErrTributeNotOwner) + } + + if assess.Status != state.AssessmentPending { + panic(ErrTributeAssessmentNotPending) + } + + assess.Status = state.AssessmentAppealed + assess.AppealReason = reason + + if err := t.putAssessment(ic.DAO, assess); err != nil { + panic(err) + } + + // Emit event + ic.AddNotification(t.Hash, TributeAppealedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(assessmentID)), + stackitem.NewByteArray([]byte(reason)), + })) + + return stackitem.NewBool(true) +} + +// grantIncentive grants circulation incentive (system/admin). +func (t *Tribute) grantIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + incentiveType := state.IncentiveType(toBigInt(args[1]).Uint64()) + amount := toBigInt(args[2]).Uint64() + reason := toString(args[3]) + + if !t.checkTributeAdmin(ic) { + panic(ErrTributeNotAdmin) + } + + if amount == 0 { + panic(ErrTributeInvalidAmount) + } + + if len(reason) == 0 || len(reason) > 256 { + panic(ErrTributeInvalidReason) + } + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + panic(ErrTributeAccountNotFound) + } + + acc, err := t.getAccountInternal(ic.DAO, vitaID) + if err != nil || acc == nil { + panic(ErrTributeAccountNotFound) + } + + // Create incentive + cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) + incentiveID := cache.incentiveCount + cache.incentiveCount++ + t.setIncentiveCounter(ic.DAO, cache.incentiveCount) + + blockHeight := ic.Block.Index + incentive := &state.CirculationIncentive{ + ID: incentiveID, + VitaID: vitaID, + Recipient: owner, + IncentiveType: incentiveType, + Amount: amount, + Reason: reason, + VelocityScore: acc.CurrentVelocity, + GrantedBlock: blockHeight, + ClaimedBlock: 0, + Claimed: false, + } + + if err := t.putIncentive(ic.DAO, incentive); err != nil { + panic(err) + } + t.addUnclaimedIncentive(ic.DAO, vitaID, incentiveID) + + // Emit event + ic.AddNotification(t.Hash, IncentiveGrantedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(vitaID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(amount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)) +} + +// claimIncentive claims a granted incentive. +func (t *Tribute) claimIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { + incentiveID := toBigInt(args[0]).Uint64() + + incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID) + if err != nil || incentive == nil { + panic(ErrTributeIncentiveNotFound) + } + + // Check caller is recipient + ok, err := checkWitness(ic, incentive.Recipient) + if err != nil || !ok { + panic(ErrTributeNotOwner) + } + + if incentive.Claimed { + panic(ErrTributeIncentiveClaimed) + } + + // Mark as claimed + incentive.Claimed = true + incentive.ClaimedBlock = ic.Block.Index + + if err := t.putIncentive(ic.DAO, incentive); err != nil { + panic(err) + } + t.removeUnclaimedIncentive(ic.DAO, incentive.VitaID, incentiveID) + + // Update account + acc, _ := t.getAccountInternal(ic.DAO, incentive.VitaID) + if acc != nil { + acc.TotalIncentivesRcvd += incentive.Amount + acc.UpdatedAt = ic.Block.Index + t.putAccount(ic.DAO, acc) + } + + // Emit event + ic.AddNotification(t.Hash, IncentiveClaimedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(incentiveID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(incentive.Amount)), + })) + + return stackitem.NewBool(true) +} + +// getIncentive returns incentive by ID. +func (t *Tribute) getIncentive(ic *interop.Context, args []stackitem.Item) stackitem.Item { + incentiveID := toBigInt(args[0]).Uint64() + + incentive, err := t.getIncentiveInternal(ic.DAO, incentiveID) + if err != nil || incentive == nil { + return stackitem.Null{} + } + + return incentive.ToStackItem() +} + +// getUnclaimedIncentives returns count of unclaimed incentives for owner. +func (t *Tribute) getUnclaimedIncentives(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + vitaID, found := t.getVitaIDByOwner(ic.DAO, owner) + if !found { + return stackitem.NewBigInteger(big.NewInt(0)) + } + + // Count unclaimed incentives by iterating storage + // This is a simplified version - in production, we'd track this counter + count := uint64(0) + prefix := make([]byte, 9) + prefix[0] = tributePrefixUnclaimedIncentive + binary.BigEndian.PutUint64(prefix[1:], vitaID) + + ic.DAO.Seek(t.ID, storage.SeekRange{Prefix: prefix}, func(k, v []byte) bool { + count++ + return true + }) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(count)) +} + +// redistribute executes wealth redistribution (committee only). +func (t *Tribute) redistribute(ic *interop.Context, args []stackitem.Item) stackitem.Item { + targetCategory := toString(args[0]) + recipientCount := toBigInt(args[1]).Uint64() + + if !t.checkCommittee(ic) { + panic(ErrTributeNotCommittee) + } + + pool := t.getTributePoolValue(ic.DAO) + if pool == 0 { + panic(ErrTributeNothingToRedistribute) + } + + if recipientCount == 0 { + panic(ErrTributeInvalidAmount) + } + + perCapita := pool / recipientCount + + // Create redistribution record + cache := ic.DAO.GetRWCache(t.ID).(*TributeCache) + redistID := cache.redistributionCount + cache.redistributionCount++ + t.setRedistributionCounter(ic.DAO, cache.redistributionCount) + + record := &state.RedistributionRecord{ + ID: redistID, + SourceAssessment: 0, // Could link to specific assessment if needed + TotalAmount: pool, + RecipientCount: recipientCount, + PerCapitaAmount: perCapita, + RedistBlock: ic.Block.Index, + TargetCategory: targetCategory, + } + + if err := t.putRedistribution(ic.DAO, record); err != nil { + panic(err) + } + + // Clear the pool (actual redistribution would happen via VTS transfers) + t.setTributePool(ic.DAO, 0) + + // Emit event + ic.AddNotification(t.Hash, RedistributionExecutedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(redistID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(pool)), + stackitem.NewBigInteger(new(big.Int).SetUint64(recipientCount)), + })) + + return stackitem.NewBigInteger(new(big.Int).SetUint64(redistID)) +} + +// getRedistribution returns redistribution record by ID. +func (t *Tribute) getRedistribution(ic *interop.Context, args []stackitem.Item) stackitem.Item { + redistID := toBigInt(args[0]).Uint64() + + rec, err := t.getRedistributionInternal(ic.DAO, redistID) + if err != nil || rec == nil { + return stackitem.Null{} + } + + return rec.ToStackItem() +} + +// getTributePool returns total tribute pool available for redistribution. +func (t *Tribute) getTributePool(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getTributePoolValue(ic.DAO))) +} + +// getConfig returns current configuration. +func (t *Tribute) getConfig(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + cfg := t.getConfigInternal(ic.DAO) + if cfg == nil { + return stackitem.Null{} + } + return cfg.ToStackItem() +} + +// getTotalAccounts returns total velocity accounts. +func (t *Tribute) getTotalAccounts(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAccountCounter(ic.DAO))) +} + +// getTotalAssessments returns total assessments. +func (t *Tribute) getTotalAssessments(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getAssessmentCounter(ic.DAO))) +} + +// getTotalIncentives returns total incentives. +func (t *Tribute) getTotalIncentives(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getIncentiveCounter(ic.DAO))) +} + +// getTotalRedistributions returns total redistributions. +func (t *Tribute) getTotalRedistributions(ic *interop.Context, _ []stackitem.Item) stackitem.Item { + return stackitem.NewBigInteger(new(big.Int).SetUint64(t.getRedistributionCounter(ic.DAO))) +} + +// ===== Public Internal Methods ===== + +// GetAccountByOwner returns velocity account by owner (internal API). +func (t *Tribute) GetAccountByOwner(d *dao.Simple, owner util.Uint160) (*state.VelocityAccount, error) { + vitaID, found := t.getVitaIDByOwner(d, owner) + if !found { + return nil, ErrTributeAccountNotFound + } + return t.getAccountInternal(d, vitaID) +} + +// GetVelocity returns velocity score for an owner (internal API). +func (t *Tribute) GetVelocity(d *dao.Simple, owner util.Uint160) uint64 { + vitaID, found := t.getVitaIDByOwner(d, owner) + if !found { + return 5000 // Default 50% + } + acc, err := t.getAccountInternal(d, vitaID) + if err != nil || acc == nil { + return 5000 + } + return acc.CurrentVelocity +} + +// IsHoarding returns true if owner is hoarding (internal API). +func (t *Tribute) IsHoarding(d *dao.Simple, owner util.Uint160) bool { + vitaID, found := t.getVitaIDByOwner(d, owner) + if !found { + return false + } + acc, err := t.getAccountInternal(d, vitaID) + if err != nil || acc == nil { + return false + } + return acc.HoardingLevel != state.HoardingNone +} + +// Address returns the contract address. +func (t *Tribute) Address() util.Uint160 { + return t.Hash +} diff --git a/pkg/core/native/validation.go b/pkg/core/native/validation.go new file mode 100644 index 0000000..da4a909 --- /dev/null +++ b/pkg/core/native/validation.go @@ -0,0 +1,128 @@ +package native + +import ( + "errors" +) + +// LOW-002: Common input validation constants and helpers for all native contracts. +// These ensure consistent validation and prevent DoS attacks via oversized inputs. + +// Maximum string lengths for input validation. +const ( + // Identity-related limits + MaxNameLength = 256 // Names of laws, roles, etc. + MaxDescriptionLength = 4096 // Detailed descriptions + MaxReasonLength = 1024 // Reasons for actions + MaxPurposeLength = 128 // Auth purposes + + // Identifiers + MaxBucketLength = 64 // Bucket/category identifiers + MaxTagLength = 128 // Tags and labels + MaxKeyLength = 256 // Attribute keys + + // Content + MaxAttributeValueLength = 4096 // Attribute values + MaxEvidenceLength = 32768 // Evidence/proof data + + // Query limits + MaxQueryLimit = 100 // Maximum items returned per query + DefaultPageSize = 20 // Default page size for pagination +) + +// Validation errors. +var ( + ErrInputTooLong = errors.New("input exceeds maximum allowed length") + ErrNameTooLong = errors.New("name exceeds maximum length") + ErrDescriptionTooLong = errors.New("description exceeds maximum length") + ErrReasonTooLong = errors.New("reason exceeds maximum length") + ErrBucketTooLong = errors.New("bucket identifier exceeds maximum length") + ErrTagTooLong = errors.New("tag exceeds maximum length") + ErrKeyTooLong = errors.New("key exceeds maximum length") + ErrValueTooLong = errors.New("value exceeds maximum length") + ErrEvidenceTooLong = errors.New("evidence exceeds maximum length") + ErrInvalidPageSize = errors.New("page size exceeds maximum") +) + +// ValidateName checks if a name is within allowed length. +func ValidateName(name string) error { + if len(name) > MaxNameLength { + return ErrNameTooLong + } + return nil +} + +// ValidateDescription checks if a description is within allowed length. +func ValidateDescription(desc string) error { + if len(desc) > MaxDescriptionLength { + return ErrDescriptionTooLong + } + return nil +} + +// ValidateReason checks if a reason is within allowed length. +func ValidateReason(reason string) error { + if len(reason) > MaxReasonLength { + return ErrReasonTooLong + } + return nil +} + +// ValidateBucket checks if a bucket identifier is within allowed length. +func ValidateBucket(bucket string) error { + if len(bucket) > MaxBucketLength { + return ErrBucketTooLong + } + return nil +} + +// ValidateTag checks if a tag is within allowed length. +func ValidateTag(tag string) error { + if len(tag) > MaxTagLength { + return ErrTagTooLong + } + return nil +} + +// ValidateKey checks if a key is within allowed length. +func ValidateKey(key string) error { + if len(key) > MaxKeyLength { + return ErrKeyTooLong + } + return nil +} + +// ValidateValue checks if a value is within allowed length. +func ValidateValue(value string) error { + if len(value) > MaxAttributeValueLength { + return ErrValueTooLong + } + return nil +} + +// ValidateEvidence checks if evidence data is within allowed length. +func ValidateEvidence(evidence []byte) error { + if len(evidence) > MaxEvidenceLength { + return ErrEvidenceTooLong + } + return nil +} + +// ValidatePageSize ensures page size is within limits. +// Returns the validated page size (clamped to max if needed). +func ValidatePageSize(size int) int { + if size <= 0 { + return DefaultPageSize + } + if size > MaxQueryLimit { + return MaxQueryLimit + } + return size +} + +// ValidateOffset ensures offset is non-negative. +func ValidateOffset(offset int) int { + if offset < 0 { + return 0 + } + return offset +} diff --git a/pkg/core/native/vita.go b/pkg/core/native/vita.go old mode 100644 new mode 100755 diff --git a/pkg/core/native/vts.go b/pkg/core/native/vts.go old mode 100644 new mode 100755 diff --git a/pkg/core/state/pons.go b/pkg/core/state/pons.go old mode 100644 new mode 100755 index 17a8c18..5c2a757 --- a/pkg/core/state/pons.go +++ b/pkg/core/state/pons.go @@ -1,130 +1,130 @@ -package state - -import ( - "github.com/tutus-one/tutus-chain/pkg/util" -) - -// AgreementStatus represents the status of a bilateral agreement. -type AgreementStatus uint8 - -// Agreement status constants. -const ( - AgreementPending AgreementStatus = 0 - AgreementActive AgreementStatus = 1 - AgreementSuspended AgreementStatus = 2 - AgreementTerminated AgreementStatus = 3 -) - -// AgreementType represents the type of bilateral agreement. -type AgreementType uint8 - -// Agreement type constants. -const ( - AgreementTypeGeneral AgreementType = 0 // General cooperation - AgreementTypeIdentity AgreementType = 1 // Identity verification - AgreementTypeSettlement AgreementType = 2 // VTS settlement - AgreementTypeEducation AgreementType = 3 // Education credential sharing - AgreementTypeHealthcare AgreementType = 4 // Healthcare record sharing - AgreementTypeComprehensive AgreementType = 5 // All services -) - -// VerificationStatus represents the status of a verification request. -type VerificationStatus uint8 - -// Verification status constants. -const ( - VerificationPending VerificationStatus = 0 - VerificationApproved VerificationStatus = 1 - VerificationRejected VerificationStatus = 2 - VerificationExpired VerificationStatus = 3 -) - -// VerificationType represents the type of verification request. -type VerificationType uint8 - -// Verification type constants. -const ( - VerificationTypeIdentity VerificationType = 0 // Identity verification - VerificationTypeCredential VerificationType = 1 // Education credential - VerificationTypeHealth VerificationType = 2 // Healthcare record - VerificationTypeCertificate VerificationType = 3 // Professional certificate -) - -// SettlementStatus represents the status of a settlement request. -type SettlementStatus uint8 - -// Settlement status constants. -const ( - SettlementPending SettlementStatus = 0 - SettlementCompleted SettlementStatus = 1 - SettlementRejected SettlementStatus = 2 - SettlementCancelled SettlementStatus = 3 -) - -// BilateralAgreement represents an agreement between two sovereign chains. -type BilateralAgreement struct { - ID uint64 // Unique agreement ID - LocalChainID uint32 // This chain's ID - RemoteChainID uint32 // Partner chain's ID - AgreementType AgreementType // Type of agreement - Status AgreementStatus - Terms util.Uint256 // Hash of off-chain terms document - EffectiveDate uint32 // Block height when effective - ExpirationDate uint32 // Block height when expires (0 = no expiry) - CreatedAt uint32 // Block height when created - UpdatedAt uint32 // Last update block height -} - -// VerificationRequest represents a cross-border verification request. -type VerificationRequest struct { - ID uint64 // Unique request ID - RequestingChain uint32 // Chain requesting verification - TargetChain uint32 // Chain being queried - Subject util.Uint160 // Subject of verification - VerificationType VerificationType - DataHash util.Uint256 // Hash of requested data - Status VerificationStatus - ResponseHash util.Uint256 // Hash of response data (if any) - Requester util.Uint160 // Who initiated request - CreatedAt uint32 // Block height - ExpiresAt uint32 // Request expiry - RespondedAt uint32 // When responded (0 = pending) -} - -// SettlementRequest represents an international VTS settlement request. -type SettlementRequest struct { - ID uint64 // Unique request ID - FromChain uint32 // Originating chain - ToChain uint32 // Destination chain - Sender util.Uint160 // Sender on from chain - Receiver util.Uint160 // Receiver on to chain - Amount uint64 // VTS amount (in smallest units) - Reference string // Payment reference - Status SettlementStatus - CreatedAt uint32 // Block height - SettledAt uint32 // When settled (0 = pending) - TxHash util.Uint256 // Settlement transaction hash -} - -// CredentialShare represents a shared credential between chains. -type CredentialShare struct { - ID uint64 // Unique share ID - SourceChain uint32 // Chain where credential originated - TargetChain uint32 // Chain receiving credential - Owner util.Uint160 // Credential owner - CredentialType VerificationType - CredentialID uint64 // Original credential ID on source chain - ContentHash util.Uint256 // Hash of credential content - ValidUntil uint32 // Validity period on target chain - CreatedAt uint32 // Block height - IsRevoked bool // Has been revoked -} - -// PonsConfig represents configurable parameters for the Pons contract. -type PonsConfig struct { - LocalChainID uint32 // This chain's unique identifier - VerificationTimeout uint32 // Blocks until verification request expires - SettlementTimeout uint32 // Blocks until settlement request expires - MaxPendingRequests uint64 // Maximum pending requests per chain - CredentialShareExpiry uint32 // Default validity period for shared credentials -} +package state + +import ( + "github.com/tutus-one/tutus-chain/pkg/util" +) + +// AgreementStatus represents the status of a bilateral agreement. +type AgreementStatus uint8 + +// Agreement status constants. +const ( + AgreementPending AgreementStatus = 0 + AgreementActive AgreementStatus = 1 + AgreementSuspended AgreementStatus = 2 + AgreementTerminated AgreementStatus = 3 +) + +// AgreementType represents the type of bilateral agreement. +type AgreementType uint8 + +// Agreement type constants. +const ( + AgreementTypeGeneral AgreementType = 0 // General cooperation + AgreementTypeIdentity AgreementType = 1 // Identity verification + AgreementTypeSettlement AgreementType = 2 // VTS settlement + AgreementTypeEducation AgreementType = 3 // Education credential sharing + AgreementTypeHealthcare AgreementType = 4 // Healthcare record sharing + AgreementTypeComprehensive AgreementType = 5 // All services +) + +// VerificationStatus represents the status of a verification request. +type VerificationStatus uint8 + +// Verification status constants. +const ( + VerificationPending VerificationStatus = 0 + VerificationApproved VerificationStatus = 1 + VerificationRejected VerificationStatus = 2 + VerificationExpired VerificationStatus = 3 +) + +// VerificationType represents the type of verification request. +type VerificationType uint8 + +// Verification type constants. +const ( + VerificationTypeIdentity VerificationType = 0 // Identity verification + VerificationTypeCredential VerificationType = 1 // Education credential + VerificationTypeHealth VerificationType = 2 // Healthcare record + VerificationTypeCertificate VerificationType = 3 // Professional certificate +) + +// SettlementStatus represents the status of a settlement request. +type SettlementStatus uint8 + +// Settlement status constants. +const ( + SettlementPending SettlementStatus = 0 + SettlementCompleted SettlementStatus = 1 + SettlementRejected SettlementStatus = 2 + SettlementCancelled SettlementStatus = 3 +) + +// BilateralAgreement represents an agreement between two sovereign chains. +type BilateralAgreement struct { + ID uint64 // Unique agreement ID + LocalChainID uint32 // This chain's ID + RemoteChainID uint32 // Partner chain's ID + AgreementType AgreementType // Type of agreement + Status AgreementStatus + Terms util.Uint256 // Hash of off-chain terms document + EffectiveDate uint32 // Block height when effective + ExpirationDate uint32 // Block height when expires (0 = no expiry) + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Last update block height +} + +// VerificationRequest represents a cross-border verification request. +type VerificationRequest struct { + ID uint64 // Unique request ID + RequestingChain uint32 // Chain requesting verification + TargetChain uint32 // Chain being queried + Subject util.Uint160 // Subject of verification + VerificationType VerificationType + DataHash util.Uint256 // Hash of requested data + Status VerificationStatus + ResponseHash util.Uint256 // Hash of response data (if any) + Requester util.Uint160 // Who initiated request + CreatedAt uint32 // Block height + ExpiresAt uint32 // Request expiry + RespondedAt uint32 // When responded (0 = pending) +} + +// SettlementRequest represents an international VTS settlement request. +type SettlementRequest struct { + ID uint64 // Unique request ID + FromChain uint32 // Originating chain + ToChain uint32 // Destination chain + Sender util.Uint160 // Sender on from chain + Receiver util.Uint160 // Receiver on to chain + Amount uint64 // VTS amount (in smallest units) + Reference string // Payment reference + Status SettlementStatus + CreatedAt uint32 // Block height + SettledAt uint32 // When settled (0 = pending) + TxHash util.Uint256 // Settlement transaction hash +} + +// CredentialShare represents a shared credential between chains. +type CredentialShare struct { + ID uint64 // Unique share ID + SourceChain uint32 // Chain where credential originated + TargetChain uint32 // Chain receiving credential + Owner util.Uint160 // Credential owner + CredentialType VerificationType + CredentialID uint64 // Original credential ID on source chain + ContentHash util.Uint256 // Hash of credential content + ValidUntil uint32 // Validity period on target chain + CreatedAt uint32 // Block height + IsRevoked bool // Has been revoked +} + +// PonsConfig represents configurable parameters for the Pons contract. +type PonsConfig struct { + LocalChainID uint32 // This chain's unique identifier + VerificationTimeout uint32 // Blocks until verification request expires + SettlementTimeout uint32 // Blocks until settlement request expires + MaxPendingRequests uint64 // Maximum pending requests per chain + CredentialShareExpiry uint32 // Default validity period for shared credentials +} diff --git a/pkg/core/state/salus.go b/pkg/core/state/salus.go old mode 100644 new mode 100755 index 48ea16a..14a3637 --- a/pkg/core/state/salus.go +++ b/pkg/core/state/salus.go @@ -1,632 +1,632 @@ -package state - -import ( - "errors" - "fmt" - "math/big" - - "github.com/tutus-one/tutus-chain/pkg/util" - "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" -) - -// HealthcareAccountStatus represents the status of a healthcare account. -type HealthcareAccountStatus uint8 - -const ( - // HealthcareAccountActive indicates an active account. - HealthcareAccountActive HealthcareAccountStatus = 0 - // HealthcareAccountSuspended indicates a temporarily suspended account. - HealthcareAccountSuspended HealthcareAccountStatus = 1 - // HealthcareAccountClosed indicates a permanently closed account. - HealthcareAccountClosed HealthcareAccountStatus = 2 -) - -// MedicalRecordType represents the type of medical record. -type MedicalRecordType uint8 - -const ( - // RecordTypeCheckup indicates a routine checkup. - RecordTypeCheckup MedicalRecordType = 0 - // RecordTypeTreatment indicates a treatment. - RecordTypeTreatment MedicalRecordType = 1 - // RecordTypeEmergency indicates an emergency visit. - RecordTypeEmergency MedicalRecordType = 2 - // RecordTypePrescription indicates a prescription. - RecordTypePrescription MedicalRecordType = 3 - // RecordTypeLabResult indicates lab results. - RecordTypeLabResult MedicalRecordType = 4 - // RecordTypeVaccination indicates a vaccination. - RecordTypeVaccination MedicalRecordType = 5 - // RecordTypeMentalHealth indicates mental health services. - RecordTypeMentalHealth MedicalRecordType = 6 - // RecordTypePreventive indicates preventive care. - RecordTypePreventive MedicalRecordType = 7 -) - -// AccessLevel represents the level of access granted to a provider. -type AccessLevel uint8 - -const ( - // AccessLevelNone indicates no access. - AccessLevelNone AccessLevel = 0 - // AccessLevelEmergency indicates emergency-only access. - AccessLevelEmergency AccessLevel = 1 - // AccessLevelLimited indicates limited access (specific record types). - AccessLevelLimited AccessLevel = 2 - // AccessLevelFull indicates full access to all records. - AccessLevelFull AccessLevel = 3 -) - -// ProviderStatus represents the status of a healthcare provider. -type ProviderStatus uint8 - -const ( - // ProviderStatusActive indicates an active provider. - ProviderStatusActive ProviderStatus = 0 - // ProviderStatusSuspended indicates a suspended provider. - ProviderStatusSuspended ProviderStatus = 1 - // ProviderStatusRevoked indicates a revoked provider. - ProviderStatusRevoked ProviderStatus = 2 -) - -// HealthcareAccount represents a citizen's healthcare account. -type HealthcareAccount struct { - VitaID uint64 // Owner's Vita token ID - Owner util.Uint160 // Owner's address - AnnualAllocation uint64 // Annual healthcare credits - CreditsUsed uint64 // Credits used this year - CreditsAvailable uint64 // Available healthcare credits - BiologicalAge uint32 // Biological age (Salus-adjusted) - LastCheckup uint32 // Block height of last checkup - Status HealthcareAccountStatus // Account status - CreatedAt uint32 // Block height when created - UpdatedAt uint32 // Block height of last update -} - -// ToStackItem implements stackitem.Convertible interface. -func (a *HealthcareAccount) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(a.VitaID))), - stackitem.NewByteArray(a.Owner.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(a.AnnualAllocation))), - stackitem.NewBigInteger(big.NewInt(int64(a.CreditsUsed))), - stackitem.NewBigInteger(big.NewInt(int64(a.CreditsAvailable))), - stackitem.NewBigInteger(big.NewInt(int64(a.BiologicalAge))), - stackitem.NewBigInteger(big.NewInt(int64(a.LastCheckup))), - stackitem.NewBigInteger(big.NewInt(int64(a.Status))), - stackitem.NewBigInteger(big.NewInt(int64(a.CreatedAt))), - stackitem.NewBigInteger(big.NewInt(int64(a.UpdatedAt))), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (a *HealthcareAccount) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 10 { - return fmt.Errorf("wrong number of elements: expected 10, got %d", len(items)) - } - - vitaID, err := items[0].TryInteger() - if err != nil { - return fmt.Errorf("invalid vitaID: %w", err) - } - a.VitaID = vitaID.Uint64() - - owner, err := items[1].TryBytes() - if err != nil { - return fmt.Errorf("invalid owner: %w", err) - } - a.Owner, err = util.Uint160DecodeBytesBE(owner) - if err != nil { - return fmt.Errorf("invalid owner address: %w", err) - } - - annualAllocation, err := items[2].TryInteger() - if err != nil { - return fmt.Errorf("invalid annualAllocation: %w", err) - } - a.AnnualAllocation = annualAllocation.Uint64() - - creditsUsed, err := items[3].TryInteger() - if err != nil { - return fmt.Errorf("invalid creditsUsed: %w", err) - } - a.CreditsUsed = creditsUsed.Uint64() - - creditsAvailable, err := items[4].TryInteger() - if err != nil { - return fmt.Errorf("invalid creditsAvailable: %w", err) - } - a.CreditsAvailable = creditsAvailable.Uint64() - - biologicalAge, err := items[5].TryInteger() - if err != nil { - return fmt.Errorf("invalid biologicalAge: %w", err) - } - a.BiologicalAge = uint32(biologicalAge.Uint64()) - - lastCheckup, err := items[6].TryInteger() - if err != nil { - return fmt.Errorf("invalid lastCheckup: %w", err) - } - a.LastCheckup = uint32(lastCheckup.Uint64()) - - status, err := items[7].TryInteger() - if err != nil { - return fmt.Errorf("invalid status: %w", err) - } - a.Status = HealthcareAccountStatus(status.Uint64()) - - createdAt, err := items[8].TryInteger() - if err != nil { - return fmt.Errorf("invalid createdAt: %w", err) - } - a.CreatedAt = uint32(createdAt.Uint64()) - - updatedAt, err := items[9].TryInteger() - if err != nil { - return fmt.Errorf("invalid updatedAt: %w", err) - } - a.UpdatedAt = uint32(updatedAt.Uint64()) - - return nil -} - -// MedicalRecord represents a medical record reference (data stored off-chain). -type MedicalRecord struct { - ID uint64 // Unique record ID - VitaID uint64 // Patient's Vita ID - Patient util.Uint160 // Patient's address - Provider util.Uint160 // Healthcare provider's address - RecordType MedicalRecordType // Type of medical record - ContentHash util.Uint256 // Hash of encrypted off-chain data - CreditsUsed uint64 // Healthcare credits used - CreatedAt uint32 // Block height when created - IsActive bool // Whether record is valid -} - -// ToStackItem implements stackitem.Convertible interface. -func (r *MedicalRecord) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(r.ID))), - stackitem.NewBigInteger(big.NewInt(int64(r.VitaID))), - stackitem.NewByteArray(r.Patient.BytesBE()), - stackitem.NewByteArray(r.Provider.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(r.RecordType))), - stackitem.NewByteArray(r.ContentHash.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(r.CreditsUsed))), - stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))), - stackitem.NewBool(r.IsActive), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (r *MedicalRecord) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 9 { - return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) - } - - id, err := items[0].TryInteger() - if err != nil { - return fmt.Errorf("invalid id: %w", err) - } - r.ID = id.Uint64() - - vitaID, err := items[1].TryInteger() - if err != nil { - return fmt.Errorf("invalid vitaID: %w", err) - } - r.VitaID = vitaID.Uint64() - - patient, err := items[2].TryBytes() - if err != nil { - return fmt.Errorf("invalid patient: %w", err) - } - r.Patient, err = util.Uint160DecodeBytesBE(patient) - if err != nil { - return fmt.Errorf("invalid patient address: %w", err) - } - - provider, err := items[3].TryBytes() - if err != nil { - return fmt.Errorf("invalid provider: %w", err) - } - r.Provider, err = util.Uint160DecodeBytesBE(provider) - if err != nil { - return fmt.Errorf("invalid provider address: %w", err) - } - - recordType, err := items[4].TryInteger() - if err != nil { - return fmt.Errorf("invalid recordType: %w", err) - } - r.RecordType = MedicalRecordType(recordType.Uint64()) - - contentHash, err := items[5].TryBytes() - if err != nil { - return fmt.Errorf("invalid contentHash: %w", err) - } - r.ContentHash, err = util.Uint256DecodeBytesBE(contentHash) - if err != nil { - return fmt.Errorf("invalid contentHash value: %w", err) - } - - creditsUsed, err := items[6].TryInteger() - if err != nil { - return fmt.Errorf("invalid creditsUsed: %w", err) - } - r.CreditsUsed = creditsUsed.Uint64() - - createdAt, err := items[7].TryInteger() - if err != nil { - return fmt.Errorf("invalid createdAt: %w", err) - } - r.CreatedAt = uint32(createdAt.Uint64()) - - isActive, err := items[8].TryBool() - if err != nil { - return fmt.Errorf("invalid isActive: %w", err) - } - r.IsActive = isActive - - return nil -} - -// ProviderAuthorization represents a healthcare provider's access authorization. -type ProviderAuthorization struct { - ID uint64 // Authorization ID - VitaID uint64 // Patient's Vita ID - Patient util.Uint160 // Patient's address - Provider util.Uint160 // Healthcare provider's address - AccessLevel AccessLevel // Level of access granted - StartsAt uint32 // Block height when access starts - ExpiresAt uint32 // Block height when access expires (0 = no expiry) - IsActive bool // Whether authorization is currently active - GrantedAt uint32 // Block height when granted -} - -// ToStackItem implements stackitem.Convertible interface. -func (p *ProviderAuthorization) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(p.ID))), - stackitem.NewBigInteger(big.NewInt(int64(p.VitaID))), - stackitem.NewByteArray(p.Patient.BytesBE()), - stackitem.NewByteArray(p.Provider.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(p.AccessLevel))), - stackitem.NewBigInteger(big.NewInt(int64(p.StartsAt))), - stackitem.NewBigInteger(big.NewInt(int64(p.ExpiresAt))), - stackitem.NewBool(p.IsActive), - stackitem.NewBigInteger(big.NewInt(int64(p.GrantedAt))), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (p *ProviderAuthorization) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 9 { - return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) - } - - id, err := items[0].TryInteger() - if err != nil { - return fmt.Errorf("invalid id: %w", err) - } - p.ID = id.Uint64() - - vitaID, err := items[1].TryInteger() - if err != nil { - return fmt.Errorf("invalid vitaID: %w", err) - } - p.VitaID = vitaID.Uint64() - - patient, err := items[2].TryBytes() - if err != nil { - return fmt.Errorf("invalid patient: %w", err) - } - p.Patient, err = util.Uint160DecodeBytesBE(patient) - if err != nil { - return fmt.Errorf("invalid patient address: %w", err) - } - - provider, err := items[3].TryBytes() - if err != nil { - return fmt.Errorf("invalid provider: %w", err) - } - p.Provider, err = util.Uint160DecodeBytesBE(provider) - if err != nil { - return fmt.Errorf("invalid provider address: %w", err) - } - - accessLevel, err := items[4].TryInteger() - if err != nil { - return fmt.Errorf("invalid accessLevel: %w", err) - } - p.AccessLevel = AccessLevel(accessLevel.Uint64()) - - startsAt, err := items[5].TryInteger() - if err != nil { - return fmt.Errorf("invalid startsAt: %w", err) - } - p.StartsAt = uint32(startsAt.Uint64()) - - expiresAt, err := items[6].TryInteger() - if err != nil { - return fmt.Errorf("invalid expiresAt: %w", err) - } - p.ExpiresAt = uint32(expiresAt.Uint64()) - - isActive, err := items[7].TryBool() - if err != nil { - return fmt.Errorf("invalid isActive: %w", err) - } - p.IsActive = isActive - - grantedAt, err := items[8].TryInteger() - if err != nil { - return fmt.Errorf("invalid grantedAt: %w", err) - } - p.GrantedAt = uint32(grantedAt.Uint64()) - - return nil -} - -// IsExpired checks if the authorization has expired. -func (p *ProviderAuthorization) IsExpired(currentBlock uint32) bool { - return p.ExpiresAt != 0 && p.ExpiresAt <= currentBlock -} - -// IsValid checks if the authorization is currently valid. -func (p *ProviderAuthorization) IsValid(currentBlock uint32) bool { - return p.IsActive && currentBlock >= p.StartsAt && !p.IsExpired(currentBlock) -} - -// HealthcareProvider represents a registered healthcare provider. -type HealthcareProvider struct { - Address util.Uint160 // Provider's address - Name string // Provider name - ProviderID uint64 // Unique provider ID - Specialty string // Medical specialty - LicenseHash util.Uint256 // Hash of license documentation - Status ProviderStatus // Provider status - RegisteredAt uint32 // Block height when registered - UpdatedAt uint32 // Block height of last update -} - -// ToStackItem implements stackitem.Convertible interface. -func (p *HealthcareProvider) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewByteArray(p.Address.BytesBE()), - stackitem.NewByteArray([]byte(p.Name)), - stackitem.NewBigInteger(big.NewInt(int64(p.ProviderID))), - stackitem.NewByteArray([]byte(p.Specialty)), - stackitem.NewByteArray(p.LicenseHash.BytesBE()), - stackitem.NewBigInteger(big.NewInt(int64(p.Status))), - stackitem.NewBigInteger(big.NewInt(int64(p.RegisteredAt))), - stackitem.NewBigInteger(big.NewInt(int64(p.UpdatedAt))), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (p *HealthcareProvider) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 8 { - return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) - } - - address, err := items[0].TryBytes() - if err != nil { - return fmt.Errorf("invalid address: %w", err) - } - p.Address, err = util.Uint160DecodeBytesBE(address) - if err != nil { - return fmt.Errorf("invalid provider address: %w", err) - } - - name, err := items[1].TryBytes() - if err != nil { - return fmt.Errorf("invalid name: %w", err) - } - p.Name = string(name) - - providerID, err := items[2].TryInteger() - if err != nil { - return fmt.Errorf("invalid providerID: %w", err) - } - p.ProviderID = providerID.Uint64() - - specialty, err := items[3].TryBytes() - if err != nil { - return fmt.Errorf("invalid specialty: %w", err) - } - p.Specialty = string(specialty) - - licenseHash, err := items[4].TryBytes() - if err != nil { - return fmt.Errorf("invalid licenseHash: %w", err) - } - p.LicenseHash, err = util.Uint256DecodeBytesBE(licenseHash) - if err != nil { - return fmt.Errorf("invalid licenseHash value: %w", err) - } - - status, err := items[5].TryInteger() - if err != nil { - return fmt.Errorf("invalid status: %w", err) - } - p.Status = ProviderStatus(status.Uint64()) - - registeredAt, err := items[6].TryInteger() - if err != nil { - return fmt.Errorf("invalid registeredAt: %w", err) - } - p.RegisteredAt = uint32(registeredAt.Uint64()) - - updatedAt, err := items[7].TryInteger() - if err != nil { - return fmt.Errorf("invalid updatedAt: %w", err) - } - p.UpdatedAt = uint32(updatedAt.Uint64()) - - return nil -} - -// EmergencyAccess represents an emergency access grant. -type EmergencyAccess struct { - ID uint64 // Emergency access ID - VitaID uint64 // Patient's Vita ID - Patient util.Uint160 // Patient's address - Provider util.Uint160 // Provider who accessed - Reason string // Emergency reason - GrantedAt uint32 // Block height when granted - ExpiresAt uint32 // Block height when expires - WasReviewed bool // Whether access was reviewed -} - -// ToStackItem implements stackitem.Convertible interface. -func (e *EmergencyAccess) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(e.ID))), - stackitem.NewBigInteger(big.NewInt(int64(e.VitaID))), - stackitem.NewByteArray(e.Patient.BytesBE()), - stackitem.NewByteArray(e.Provider.BytesBE()), - stackitem.NewByteArray([]byte(e.Reason)), - stackitem.NewBigInteger(big.NewInt(int64(e.GrantedAt))), - stackitem.NewBigInteger(big.NewInt(int64(e.ExpiresAt))), - stackitem.NewBool(e.WasReviewed), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (e *EmergencyAccess) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 8 { - return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) - } - - id, err := items[0].TryInteger() - if err != nil { - return fmt.Errorf("invalid id: %w", err) - } - e.ID = id.Uint64() - - vitaID, err := items[1].TryInteger() - if err != nil { - return fmt.Errorf("invalid vitaID: %w", err) - } - e.VitaID = vitaID.Uint64() - - patient, err := items[2].TryBytes() - if err != nil { - return fmt.Errorf("invalid patient: %w", err) - } - e.Patient, err = util.Uint160DecodeBytesBE(patient) - if err != nil { - return fmt.Errorf("invalid patient address: %w", err) - } - - provider, err := items[3].TryBytes() - if err != nil { - return fmt.Errorf("invalid provider: %w", err) - } - e.Provider, err = util.Uint160DecodeBytesBE(provider) - if err != nil { - return fmt.Errorf("invalid provider address: %w", err) - } - - reason, err := items[4].TryBytes() - if err != nil { - return fmt.Errorf("invalid reason: %w", err) - } - e.Reason = string(reason) - - grantedAt, err := items[5].TryInteger() - if err != nil { - return fmt.Errorf("invalid grantedAt: %w", err) - } - e.GrantedAt = uint32(grantedAt.Uint64()) - - expiresAt, err := items[6].TryInteger() - if err != nil { - return fmt.Errorf("invalid expiresAt: %w", err) - } - e.ExpiresAt = uint32(expiresAt.Uint64()) - - wasReviewed, err := items[7].TryBool() - if err != nil { - return fmt.Errorf("invalid wasReviewed: %w", err) - } - e.WasReviewed = wasReviewed - - return nil -} - -// SalusConfig represents configurable parameters for the Salus contract. -type SalusConfig struct { - DefaultAnnualCredits uint64 // Default annual healthcare credits - EmergencyAccessDuration uint32 // Blocks for emergency access (default ~24 hours) - PreventiveCareBonus uint64 // Bonus credits for preventive care - MaxAuthorizationDuration uint32 // Maximum authorization duration in blocks -} - -// ToStackItem implements stackitem.Convertible interface. -func (c *SalusConfig) ToStackItem() (stackitem.Item, error) { - return stackitem.NewStruct([]stackitem.Item{ - stackitem.NewBigInteger(big.NewInt(int64(c.DefaultAnnualCredits))), - stackitem.NewBigInteger(big.NewInt(int64(c.EmergencyAccessDuration))), - stackitem.NewBigInteger(big.NewInt(int64(c.PreventiveCareBonus))), - stackitem.NewBigInteger(big.NewInt(int64(c.MaxAuthorizationDuration))), - }), nil -} - -// FromStackItem implements stackitem.Convertible interface. -func (c *SalusConfig) FromStackItem(item stackitem.Item) error { - items, ok := item.Value().([]stackitem.Item) - if !ok { - return errors.New("not a struct") - } - if len(items) != 4 { - return fmt.Errorf("wrong number of elements: expected 4, got %d", len(items)) - } - - defaultCredits, err := items[0].TryInteger() - if err != nil { - return fmt.Errorf("invalid defaultAnnualCredits: %w", err) - } - c.DefaultAnnualCredits = defaultCredits.Uint64() - - emergencyDuration, err := items[1].TryInteger() - if err != nil { - return fmt.Errorf("invalid emergencyAccessDuration: %w", err) - } - c.EmergencyAccessDuration = uint32(emergencyDuration.Uint64()) - - preventiveBonus, err := items[2].TryInteger() - if err != nil { - return fmt.Errorf("invalid preventiveCareBonus: %w", err) - } - c.PreventiveCareBonus = preventiveBonus.Uint64() - - maxAuthDuration, err := items[3].TryInteger() - if err != nil { - return fmt.Errorf("invalid maxAuthorizationDuration: %w", err) - } - c.MaxAuthorizationDuration = uint32(maxAuthDuration.Uint64()) - - return nil -} +package state + +import ( + "errors" + "fmt" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// HealthcareAccountStatus represents the status of a healthcare account. +type HealthcareAccountStatus uint8 + +const ( + // HealthcareAccountActive indicates an active account. + HealthcareAccountActive HealthcareAccountStatus = 0 + // HealthcareAccountSuspended indicates a temporarily suspended account. + HealthcareAccountSuspended HealthcareAccountStatus = 1 + // HealthcareAccountClosed indicates a permanently closed account. + HealthcareAccountClosed HealthcareAccountStatus = 2 +) + +// MedicalRecordType represents the type of medical record. +type MedicalRecordType uint8 + +const ( + // RecordTypeCheckup indicates a routine checkup. + RecordTypeCheckup MedicalRecordType = 0 + // RecordTypeTreatment indicates a treatment. + RecordTypeTreatment MedicalRecordType = 1 + // RecordTypeEmergency indicates an emergency visit. + RecordTypeEmergency MedicalRecordType = 2 + // RecordTypePrescription indicates a prescription. + RecordTypePrescription MedicalRecordType = 3 + // RecordTypeLabResult indicates lab results. + RecordTypeLabResult MedicalRecordType = 4 + // RecordTypeVaccination indicates a vaccination. + RecordTypeVaccination MedicalRecordType = 5 + // RecordTypeMentalHealth indicates mental health services. + RecordTypeMentalHealth MedicalRecordType = 6 + // RecordTypePreventive indicates preventive care. + RecordTypePreventive MedicalRecordType = 7 +) + +// AccessLevel represents the level of access granted to a provider. +type AccessLevel uint8 + +const ( + // AccessLevelNone indicates no access. + AccessLevelNone AccessLevel = 0 + // AccessLevelEmergency indicates emergency-only access. + AccessLevelEmergency AccessLevel = 1 + // AccessLevelLimited indicates limited access (specific record types). + AccessLevelLimited AccessLevel = 2 + // AccessLevelFull indicates full access to all records. + AccessLevelFull AccessLevel = 3 +) + +// ProviderStatus represents the status of a healthcare provider. +type ProviderStatus uint8 + +const ( + // ProviderStatusActive indicates an active provider. + ProviderStatusActive ProviderStatus = 0 + // ProviderStatusSuspended indicates a suspended provider. + ProviderStatusSuspended ProviderStatus = 1 + // ProviderStatusRevoked indicates a revoked provider. + ProviderStatusRevoked ProviderStatus = 2 +) + +// HealthcareAccount represents a citizen's healthcare account. +type HealthcareAccount struct { + VitaID uint64 // Owner's Vita token ID + Owner util.Uint160 // Owner's address + AnnualAllocation uint64 // Annual healthcare credits + CreditsUsed uint64 // Credits used this year + CreditsAvailable uint64 // Available healthcare credits + BiologicalAge uint32 // Biological age (Salus-adjusted) + LastCheckup uint32 // Block height of last checkup + Status HealthcareAccountStatus // Account status + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Block height of last update +} + +// ToStackItem implements stackitem.Convertible interface. +func (a *HealthcareAccount) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(a.VitaID))), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(a.AnnualAllocation))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreditsUsed))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreditsAvailable))), + stackitem.NewBigInteger(big.NewInt(int64(a.BiologicalAge))), + stackitem.NewBigInteger(big.NewInt(int64(a.LastCheckup))), + stackitem.NewBigInteger(big.NewInt(int64(a.Status))), + stackitem.NewBigInteger(big.NewInt(int64(a.CreatedAt))), + stackitem.NewBigInteger(big.NewInt(int64(a.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (a *HealthcareAccount) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 10 { + return fmt.Errorf("wrong number of elements: expected 10, got %d", len(items)) + } + + vitaID, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + a.VitaID = vitaID.Uint64() + + owner, err := items[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid owner: %w", err) + } + a.Owner, err = util.Uint160DecodeBytesBE(owner) + if err != nil { + return fmt.Errorf("invalid owner address: %w", err) + } + + annualAllocation, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid annualAllocation: %w", err) + } + a.AnnualAllocation = annualAllocation.Uint64() + + creditsUsed, err := items[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsUsed: %w", err) + } + a.CreditsUsed = creditsUsed.Uint64() + + creditsAvailable, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsAvailable: %w", err) + } + a.CreditsAvailable = creditsAvailable.Uint64() + + biologicalAge, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid biologicalAge: %w", err) + } + a.BiologicalAge = uint32(biologicalAge.Uint64()) + + lastCheckup, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid lastCheckup: %w", err) + } + a.LastCheckup = uint32(lastCheckup.Uint64()) + + status, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + a.Status = HealthcareAccountStatus(status.Uint64()) + + createdAt, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + a.CreatedAt = uint32(createdAt.Uint64()) + + updatedAt, err := items[9].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + a.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// MedicalRecord represents a medical record reference (data stored off-chain). +type MedicalRecord struct { + ID uint64 // Unique record ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Healthcare provider's address + RecordType MedicalRecordType // Type of medical record + ContentHash util.Uint256 // Hash of encrypted off-chain data + CreditsUsed uint64 // Healthcare credits used + CreatedAt uint32 // Block height when created + IsActive bool // Whether record is valid +} + +// ToStackItem implements stackitem.Convertible interface. +func (r *MedicalRecord) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(r.ID))), + stackitem.NewBigInteger(big.NewInt(int64(r.VitaID))), + stackitem.NewByteArray(r.Patient.BytesBE()), + stackitem.NewByteArray(r.Provider.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(r.RecordType))), + stackitem.NewByteArray(r.ContentHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(r.CreditsUsed))), + stackitem.NewBigInteger(big.NewInt(int64(r.CreatedAt))), + stackitem.NewBool(r.IsActive), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (r *MedicalRecord) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 9 { + return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + r.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + r.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + r.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + r.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + recordType, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid recordType: %w", err) + } + r.RecordType = MedicalRecordType(recordType.Uint64()) + + contentHash, err := items[5].TryBytes() + if err != nil { + return fmt.Errorf("invalid contentHash: %w", err) + } + r.ContentHash, err = util.Uint256DecodeBytesBE(contentHash) + if err != nil { + return fmt.Errorf("invalid contentHash value: %w", err) + } + + creditsUsed, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid creditsUsed: %w", err) + } + r.CreditsUsed = creditsUsed.Uint64() + + createdAt, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid createdAt: %w", err) + } + r.CreatedAt = uint32(createdAt.Uint64()) + + isActive, err := items[8].TryBool() + if err != nil { + return fmt.Errorf("invalid isActive: %w", err) + } + r.IsActive = isActive + + return nil +} + +// ProviderAuthorization represents a healthcare provider's access authorization. +type ProviderAuthorization struct { + ID uint64 // Authorization ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Healthcare provider's address + AccessLevel AccessLevel // Level of access granted + StartsAt uint32 // Block height when access starts + ExpiresAt uint32 // Block height when access expires (0 = no expiry) + IsActive bool // Whether authorization is currently active + GrantedAt uint32 // Block height when granted +} + +// ToStackItem implements stackitem.Convertible interface. +func (p *ProviderAuthorization) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(p.ID))), + stackitem.NewBigInteger(big.NewInt(int64(p.VitaID))), + stackitem.NewByteArray(p.Patient.BytesBE()), + stackitem.NewByteArray(p.Provider.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(p.AccessLevel))), + stackitem.NewBigInteger(big.NewInt(int64(p.StartsAt))), + stackitem.NewBigInteger(big.NewInt(int64(p.ExpiresAt))), + stackitem.NewBool(p.IsActive), + stackitem.NewBigInteger(big.NewInt(int64(p.GrantedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (p *ProviderAuthorization) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 9 { + return fmt.Errorf("wrong number of elements: expected 9, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + p.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + p.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + p.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + p.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + accessLevel, err := items[4].TryInteger() + if err != nil { + return fmt.Errorf("invalid accessLevel: %w", err) + } + p.AccessLevel = AccessLevel(accessLevel.Uint64()) + + startsAt, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid startsAt: %w", err) + } + p.StartsAt = uint32(startsAt.Uint64()) + + expiresAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid expiresAt: %w", err) + } + p.ExpiresAt = uint32(expiresAt.Uint64()) + + isActive, err := items[7].TryBool() + if err != nil { + return fmt.Errorf("invalid isActive: %w", err) + } + p.IsActive = isActive + + grantedAt, err := items[8].TryInteger() + if err != nil { + return fmt.Errorf("invalid grantedAt: %w", err) + } + p.GrantedAt = uint32(grantedAt.Uint64()) + + return nil +} + +// IsExpired checks if the authorization has expired. +func (p *ProviderAuthorization) IsExpired(currentBlock uint32) bool { + return p.ExpiresAt != 0 && p.ExpiresAt <= currentBlock +} + +// IsValid checks if the authorization is currently valid. +func (p *ProviderAuthorization) IsValid(currentBlock uint32) bool { + return p.IsActive && currentBlock >= p.StartsAt && !p.IsExpired(currentBlock) +} + +// HealthcareProvider represents a registered healthcare provider. +type HealthcareProvider struct { + Address util.Uint160 // Provider's address + Name string // Provider name + ProviderID uint64 // Unique provider ID + Specialty string // Medical specialty + LicenseHash util.Uint256 // Hash of license documentation + Status ProviderStatus // Provider status + RegisteredAt uint32 // Block height when registered + UpdatedAt uint32 // Block height of last update +} + +// ToStackItem implements stackitem.Convertible interface. +func (p *HealthcareProvider) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(p.Address.BytesBE()), + stackitem.NewByteArray([]byte(p.Name)), + stackitem.NewBigInteger(big.NewInt(int64(p.ProviderID))), + stackitem.NewByteArray([]byte(p.Specialty)), + stackitem.NewByteArray(p.LicenseHash.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(p.Status))), + stackitem.NewBigInteger(big.NewInt(int64(p.RegisteredAt))), + stackitem.NewBigInteger(big.NewInt(int64(p.UpdatedAt))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (p *HealthcareProvider) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 8 { + return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) + } + + address, err := items[0].TryBytes() + if err != nil { + return fmt.Errorf("invalid address: %w", err) + } + p.Address, err = util.Uint160DecodeBytesBE(address) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + name, err := items[1].TryBytes() + if err != nil { + return fmt.Errorf("invalid name: %w", err) + } + p.Name = string(name) + + providerID, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid providerID: %w", err) + } + p.ProviderID = providerID.Uint64() + + specialty, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid specialty: %w", err) + } + p.Specialty = string(specialty) + + licenseHash, err := items[4].TryBytes() + if err != nil { + return fmt.Errorf("invalid licenseHash: %w", err) + } + p.LicenseHash, err = util.Uint256DecodeBytesBE(licenseHash) + if err != nil { + return fmt.Errorf("invalid licenseHash value: %w", err) + } + + status, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid status: %w", err) + } + p.Status = ProviderStatus(status.Uint64()) + + registeredAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid registeredAt: %w", err) + } + p.RegisteredAt = uint32(registeredAt.Uint64()) + + updatedAt, err := items[7].TryInteger() + if err != nil { + return fmt.Errorf("invalid updatedAt: %w", err) + } + p.UpdatedAt = uint32(updatedAt.Uint64()) + + return nil +} + +// EmergencyAccess represents an emergency access grant. +type EmergencyAccess struct { + ID uint64 // Emergency access ID + VitaID uint64 // Patient's Vita ID + Patient util.Uint160 // Patient's address + Provider util.Uint160 // Provider who accessed + Reason string // Emergency reason + GrantedAt uint32 // Block height when granted + ExpiresAt uint32 // Block height when expires + WasReviewed bool // Whether access was reviewed +} + +// ToStackItem implements stackitem.Convertible interface. +func (e *EmergencyAccess) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(e.ID))), + stackitem.NewBigInteger(big.NewInt(int64(e.VitaID))), + stackitem.NewByteArray(e.Patient.BytesBE()), + stackitem.NewByteArray(e.Provider.BytesBE()), + stackitem.NewByteArray([]byte(e.Reason)), + stackitem.NewBigInteger(big.NewInt(int64(e.GrantedAt))), + stackitem.NewBigInteger(big.NewInt(int64(e.ExpiresAt))), + stackitem.NewBool(e.WasReviewed), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (e *EmergencyAccess) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 8 { + return fmt.Errorf("wrong number of elements: expected 8, got %d", len(items)) + } + + id, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + e.ID = id.Uint64() + + vitaID, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid vitaID: %w", err) + } + e.VitaID = vitaID.Uint64() + + patient, err := items[2].TryBytes() + if err != nil { + return fmt.Errorf("invalid patient: %w", err) + } + e.Patient, err = util.Uint160DecodeBytesBE(patient) + if err != nil { + return fmt.Errorf("invalid patient address: %w", err) + } + + provider, err := items[3].TryBytes() + if err != nil { + return fmt.Errorf("invalid provider: %w", err) + } + e.Provider, err = util.Uint160DecodeBytesBE(provider) + if err != nil { + return fmt.Errorf("invalid provider address: %w", err) + } + + reason, err := items[4].TryBytes() + if err != nil { + return fmt.Errorf("invalid reason: %w", err) + } + e.Reason = string(reason) + + grantedAt, err := items[5].TryInteger() + if err != nil { + return fmt.Errorf("invalid grantedAt: %w", err) + } + e.GrantedAt = uint32(grantedAt.Uint64()) + + expiresAt, err := items[6].TryInteger() + if err != nil { + return fmt.Errorf("invalid expiresAt: %w", err) + } + e.ExpiresAt = uint32(expiresAt.Uint64()) + + wasReviewed, err := items[7].TryBool() + if err != nil { + return fmt.Errorf("invalid wasReviewed: %w", err) + } + e.WasReviewed = wasReviewed + + return nil +} + +// SalusConfig represents configurable parameters for the Salus contract. +type SalusConfig struct { + DefaultAnnualCredits uint64 // Default annual healthcare credits + EmergencyAccessDuration uint32 // Blocks for emergency access (default ~24 hours) + PreventiveCareBonus uint64 // Bonus credits for preventive care + MaxAuthorizationDuration uint32 // Maximum authorization duration in blocks +} + +// ToStackItem implements stackitem.Convertible interface. +func (c *SalusConfig) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(c.DefaultAnnualCredits))), + stackitem.NewBigInteger(big.NewInt(int64(c.EmergencyAccessDuration))), + stackitem.NewBigInteger(big.NewInt(int64(c.PreventiveCareBonus))), + stackitem.NewBigInteger(big.NewInt(int64(c.MaxAuthorizationDuration))), + }), nil +} + +// FromStackItem implements stackitem.Convertible interface. +func (c *SalusConfig) FromStackItem(item stackitem.Item) error { + items, ok := item.Value().([]stackitem.Item) + if !ok { + return errors.New("not a struct") + } + if len(items) != 4 { + return fmt.Errorf("wrong number of elements: expected 4, got %d", len(items)) + } + + defaultCredits, err := items[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid defaultAnnualCredits: %w", err) + } + c.DefaultAnnualCredits = defaultCredits.Uint64() + + emergencyDuration, err := items[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid emergencyAccessDuration: %w", err) + } + c.EmergencyAccessDuration = uint32(emergencyDuration.Uint64()) + + preventiveBonus, err := items[2].TryInteger() + if err != nil { + return fmt.Errorf("invalid preventiveCareBonus: %w", err) + } + c.PreventiveCareBonus = preventiveBonus.Uint64() + + maxAuthDuration, err := items[3].TryInteger() + if err != nil { + return fmt.Errorf("invalid maxAuthorizationDuration: %w", err) + } + c.MaxAuthorizationDuration = uint32(maxAuthDuration.Uint64()) + + return nil +} diff --git a/pkg/core/state/tribute.go b/pkg/core/state/tribute.go old mode 100644 new mode 100755 index 09f93b1..faf3483 --- a/pkg/core/state/tribute.go +++ b/pkg/core/state/tribute.go @@ -1,647 +1,647 @@ -package state - -import ( - "errors" - "math/big" - - "github.com/tutus-one/tutus-chain/pkg/util" - "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" -) - -// VelocityAccountStatus represents the velocity tracking account status. -type VelocityAccountStatus uint8 - -const ( - // VelocityAccountActive indicates the account is actively tracked. - VelocityAccountActive VelocityAccountStatus = 0 - // VelocityAccountExempt indicates the account is exempt from tribute. - VelocityAccountExempt VelocityAccountStatus = 1 - // VelocityAccountSuspended indicates velocity tracking is suspended. - VelocityAccountSuspended VelocityAccountStatus = 2 -) - -// AssessmentStatus represents the status of a tribute assessment. -type AssessmentStatus uint8 - -const ( - // AssessmentPending indicates the assessment is pending collection. - AssessmentPending AssessmentStatus = 0 - // AssessmentCollected indicates the tribute has been collected. - AssessmentCollected AssessmentStatus = 1 - // AssessmentWaived indicates the assessment was waived. - AssessmentWaived AssessmentStatus = 2 - // AssessmentAppealed indicates the assessment is under appeal. - AssessmentAppealed AssessmentStatus = 3 -) - -// HoardingLevel represents the severity of resource hoarding. -type HoardingLevel uint8 - -const ( - // HoardingNone indicates no hoarding detected. - HoardingNone HoardingLevel = 0 - // HoardingMild indicates mild hoarding (below normal velocity). - HoardingMild HoardingLevel = 1 - // HoardingModerate indicates moderate hoarding. - HoardingModerate HoardingLevel = 2 - // HoardingSevere indicates severe hoarding. - HoardingSevere HoardingLevel = 3 - // HoardingExtreme indicates extreme hoarding (stagnant resources). - HoardingExtreme HoardingLevel = 4 -) - -// IncentiveType represents types of circulation incentives. -type IncentiveType uint8 - -const ( - // IncentiveVelocityBonus rewards high resource velocity. - IncentiveVelocityBonus IncentiveType = 0 - // IncentiveProductiveUse rewards productive resource use. - IncentiveProductiveUse IncentiveType = 1 - // IncentiveCommunitySupport rewards community contributions. - IncentiveCommunitySupport IncentiveType = 2 - // IncentiveEducationSpending rewards education investment. - IncentiveEducationSpending IncentiveType = 3 - // IncentiveHealthcareSpending rewards healthcare investment. - IncentiveHealthcareSpending IncentiveType = 4 -) - -// VelocityAccount tracks resource velocity for a Vita holder. -type VelocityAccount struct { - VitaID uint64 // Owner's Vita token ID - Owner util.Uint160 // Owner's address - CurrentVelocity uint64 // Current velocity score (0-10000 basis points) - AverageVelocity uint64 // Rolling average velocity - LastActivityBlock uint32 // Last transaction block - TotalInflow uint64 // Total resources received - TotalOutflow uint64 // Total resources spent/transferred - StagnantBalance uint64 // Balance considered stagnant - HoardingLevel HoardingLevel // Current hoarding assessment - ExemptionReason string // Reason for exemption (if any) - TotalTributePaid uint64 // Lifetime tribute paid - TotalIncentivesRcvd uint64 // Lifetime incentives received - Status VelocityAccountStatus // Account status - CreatedAt uint32 // Block height when created - UpdatedAt uint32 // Last update block -} - -// ToStackItem implements stackitem.Convertible. -func (a *VelocityAccount) ToStackItem() stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), - stackitem.NewByteArray(a.Owner.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.CurrentVelocity)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.AverageVelocity)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.LastActivityBlock))), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalInflow)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalOutflow)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantBalance)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), - stackitem.NewByteArray([]byte(a.ExemptionReason)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalTributePaid)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalIncentivesRcvd)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CreatedAt))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.UpdatedAt))), - }) -} - -// FromStackItem implements stackitem.Convertible. -func (a *VelocityAccount) FromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok || len(arr) < 15 { - return errors.New("not a struct") - } - vitaID, err := arr[0].TryInteger() - if err != nil { - return err - } - a.VitaID = vitaID.Uint64() - - ownerBytes, err := arr[1].TryBytes() - if err != nil { - return err - } - a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) - if err != nil { - return err - } - - currentVel, err := arr[2].TryInteger() - if err != nil { - return err - } - a.CurrentVelocity = currentVel.Uint64() - - avgVel, err := arr[3].TryInteger() - if err != nil { - return err - } - a.AverageVelocity = avgVel.Uint64() - - lastActivity, err := arr[4].TryInteger() - if err != nil { - return err - } - a.LastActivityBlock = uint32(lastActivity.Uint64()) - - totalIn, err := arr[5].TryInteger() - if err != nil { - return err - } - a.TotalInflow = totalIn.Uint64() - - totalOut, err := arr[6].TryInteger() - if err != nil { - return err - } - a.TotalOutflow = totalOut.Uint64() - - stagnant, err := arr[7].TryInteger() - if err != nil { - return err - } - a.StagnantBalance = stagnant.Uint64() - - hoardLevel, err := arr[8].TryInteger() - if err != nil { - return err - } - a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) - - exemptReason, err := arr[9].TryBytes() - if err != nil { - return err - } - a.ExemptionReason = string(exemptReason) - - totalTrib, err := arr[10].TryInteger() - if err != nil { - return err - } - a.TotalTributePaid = totalTrib.Uint64() - - totalInc, err := arr[11].TryInteger() - if err != nil { - return err - } - a.TotalIncentivesRcvd = totalInc.Uint64() - - status, err := arr[12].TryInteger() - if err != nil { - return err - } - a.Status = VelocityAccountStatus(status.Uint64()) - - created, err := arr[13].TryInteger() - if err != nil { - return err - } - a.CreatedAt = uint32(created.Uint64()) - - updated, err := arr[14].TryInteger() - if err != nil { - return err - } - a.UpdatedAt = uint32(updated.Uint64()) - - return nil -} - -// TributeAssessment represents a hoarding tribute assessment. -type TributeAssessment struct { - ID uint64 // Unique assessment ID - VitaID uint64 // Owner's Vita ID - Owner util.Uint160 // Owner's address - AssessmentBlock uint32 // Block when assessment made - HoardingLevel HoardingLevel // Hoarding level at assessment - StagnantAmount uint64 // Amount considered stagnant - TributeRate uint64 // Rate applied (basis points) - TributeAmount uint64 // Tribute amount due - DueBlock uint32 // Block by which tribute is due - CollectedBlock uint32 // Block when collected (0 if not) - Status AssessmentStatus // Assessment status - AppealReason string // Reason for appeal (if any) -} - -// ToStackItem implements stackitem.Convertible. -func (a *TributeAssessment) ToStackItem() stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(a.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), - stackitem.NewByteArray(a.Owner.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.AssessmentBlock))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantAmount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeRate)), - stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeAmount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.DueBlock))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CollectedBlock))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), - stackitem.NewByteArray([]byte(a.AppealReason)), - }) -} - -// FromStackItem implements stackitem.Convertible. -func (a *TributeAssessment) FromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok || len(arr) < 12 { - return errors.New("not a struct") - } - - id, err := arr[0].TryInteger() - if err != nil { - return err - } - a.ID = id.Uint64() - - vitaID, err := arr[1].TryInteger() - if err != nil { - return err - } - a.VitaID = vitaID.Uint64() - - ownerBytes, err := arr[2].TryBytes() - if err != nil { - return err - } - a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) - if err != nil { - return err - } - - assessBlock, err := arr[3].TryInteger() - if err != nil { - return err - } - a.AssessmentBlock = uint32(assessBlock.Uint64()) - - hoardLevel, err := arr[4].TryInteger() - if err != nil { - return err - } - a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) - - stagnant, err := arr[5].TryInteger() - if err != nil { - return err - } - a.StagnantAmount = stagnant.Uint64() - - rate, err := arr[6].TryInteger() - if err != nil { - return err - } - a.TributeRate = rate.Uint64() - - amount, err := arr[7].TryInteger() - if err != nil { - return err - } - a.TributeAmount = amount.Uint64() - - due, err := arr[8].TryInteger() - if err != nil { - return err - } - a.DueBlock = uint32(due.Uint64()) - - collected, err := arr[9].TryInteger() - if err != nil { - return err - } - a.CollectedBlock = uint32(collected.Uint64()) - - status, err := arr[10].TryInteger() - if err != nil { - return err - } - a.Status = AssessmentStatus(status.Uint64()) - - appealReason, err := arr[11].TryBytes() - if err != nil { - return err - } - a.AppealReason = string(appealReason) - - return nil -} - -// CirculationIncentive represents a reward for resource circulation. -type CirculationIncentive struct { - ID uint64 // Unique incentive ID - VitaID uint64 // Recipient's Vita ID - Recipient util.Uint160 // Recipient's address - IncentiveType IncentiveType // Type of incentive - Amount uint64 // Incentive amount - Reason string // Reason for incentive - VelocityScore uint64 // Velocity score that triggered incentive - GrantedBlock uint32 // Block when granted - ClaimedBlock uint32 // Block when claimed (0 if not) - Claimed bool // Whether incentive was claimed -} - -// ToStackItem implements stackitem.Convertible. -func (i *CirculationIncentive) ToStackItem() stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(i.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(i.VitaID)), - stackitem.NewByteArray(i.Recipient.BytesBE()), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.IncentiveType))), - stackitem.NewBigInteger(new(big.Int).SetUint64(i.Amount)), - stackitem.NewByteArray([]byte(i.Reason)), - stackitem.NewBigInteger(new(big.Int).SetUint64(i.VelocityScore)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.GrantedBlock))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.ClaimedBlock))), - stackitem.NewBool(i.Claimed), - }) -} - -// FromStackItem implements stackitem.Convertible. -func (i *CirculationIncentive) FromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok || len(arr) < 10 { - return errors.New("not a struct") - } - - id, err := arr[0].TryInteger() - if err != nil { - return err - } - i.ID = id.Uint64() - - vitaID, err := arr[1].TryInteger() - if err != nil { - return err - } - i.VitaID = vitaID.Uint64() - - recipBytes, err := arr[2].TryBytes() - if err != nil { - return err - } - i.Recipient, err = util.Uint160DecodeBytesBE(recipBytes) - if err != nil { - return err - } - - incType, err := arr[3].TryInteger() - if err != nil { - return err - } - i.IncentiveType = IncentiveType(incType.Uint64()) - - amount, err := arr[4].TryInteger() - if err != nil { - return err - } - i.Amount = amount.Uint64() - - reason, err := arr[5].TryBytes() - if err != nil { - return err - } - i.Reason = string(reason) - - velScore, err := arr[6].TryInteger() - if err != nil { - return err - } - i.VelocityScore = velScore.Uint64() - - granted, err := arr[7].TryInteger() - if err != nil { - return err - } - i.GrantedBlock = uint32(granted.Uint64()) - - claimed, err := arr[8].TryInteger() - if err != nil { - return err - } - i.ClaimedBlock = uint32(claimed.Uint64()) - - claimedBool, err := arr[9].TryBool() - if err != nil { - return err - } - i.Claimed = claimedBool - - return nil -} - -// RedistributionRecord tracks wealth redistribution events. -type RedistributionRecord struct { - ID uint64 // Unique record ID - SourceAssessment uint64 // Source assessment ID - TotalAmount uint64 // Total amount redistributed - RecipientCount uint64 // Number of recipients - PerCapitaAmount uint64 // Amount per recipient - RedistBlock uint32 // Block when redistribution occurred - TargetCategory string // Category of recipients (e.g., "low_velocity", "all_citizens") -} - -// ToStackItem implements stackitem.Convertible. -func (r *RedistributionRecord) ToStackItem() stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(r.ID)), - stackitem.NewBigInteger(new(big.Int).SetUint64(r.SourceAssessment)), - stackitem.NewBigInteger(new(big.Int).SetUint64(r.TotalAmount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(r.RecipientCount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(r.PerCapitaAmount)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(r.RedistBlock))), - stackitem.NewByteArray([]byte(r.TargetCategory)), - }) -} - -// FromStackItem implements stackitem.Convertible. -func (r *RedistributionRecord) FromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok || len(arr) < 7 { - return errors.New("not a struct") - } - - id, err := arr[0].TryInteger() - if err != nil { - return err - } - r.ID = id.Uint64() - - sourceAssess, err := arr[1].TryInteger() - if err != nil { - return err - } - r.SourceAssessment = sourceAssess.Uint64() - - total, err := arr[2].TryInteger() - if err != nil { - return err - } - r.TotalAmount = total.Uint64() - - recipCount, err := arr[3].TryInteger() - if err != nil { - return err - } - r.RecipientCount = recipCount.Uint64() - - perCapita, err := arr[4].TryInteger() - if err != nil { - return err - } - r.PerCapitaAmount = perCapita.Uint64() - - redistBlock, err := arr[5].TryInteger() - if err != nil { - return err - } - r.RedistBlock = uint32(redistBlock.Uint64()) - - targetCat, err := arr[6].TryBytes() - if err != nil { - return err - } - r.TargetCategory = string(targetCat) - - return nil -} - -// TributeConfig holds configurable parameters for the Tribute system. -type TributeConfig struct { - VelocityThresholdMild uint64 // Velocity below this = mild hoarding (basis points) - VelocityThresholdModerate uint64 // Velocity below this = moderate hoarding - VelocityThresholdSevere uint64 // Velocity below this = severe hoarding - VelocityThresholdExtreme uint64 // Velocity below this = extreme hoarding - TributeRateMild uint64 // Tribute rate for mild hoarding (basis points) - TributeRateModerate uint64 // Tribute rate for moderate hoarding - TributeRateSevere uint64 // Tribute rate for severe hoarding - TributeRateExtreme uint64 // Tribute rate for extreme hoarding - IncentiveRateHigh uint64 // Incentive rate for high velocity - IncentiveRateVeryHigh uint64 // Incentive rate for very high velocity - StagnancyPeriod uint32 // Blocks before balance considered stagnant - AssessmentPeriod uint32 // Blocks between assessments - GracePeriod uint32 // Blocks before tribute due after assessment - MinBalanceForTribute uint64 // Minimum balance to assess tribute - ExemptionThreshold uint64 // Balance below which exempt from tribute -} - -// ToStackItem implements stackitem.Convertible. -func (c *TributeConfig) ToStackItem() stackitem.Item { - return stackitem.NewArray([]stackitem.Item{ - stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdMild)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdModerate)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdSevere)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdExtreme)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateMild)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateModerate)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateSevere)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateExtreme)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateHigh)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateVeryHigh)), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.StagnancyPeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.AssessmentPeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.GracePeriod))), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.MinBalanceForTribute)), - stackitem.NewBigInteger(new(big.Int).SetUint64(c.ExemptionThreshold)), - }) -} - -// FromStackItem implements stackitem.Convertible. -func (c *TributeConfig) FromStackItem(item stackitem.Item) error { - arr, ok := item.Value().([]stackitem.Item) - if !ok || len(arr) < 15 { - return errors.New("not a struct") - } - - threshMild, err := arr[0].TryInteger() - if err != nil { - return err - } - c.VelocityThresholdMild = threshMild.Uint64() - - threshMod, err := arr[1].TryInteger() - if err != nil { - return err - } - c.VelocityThresholdModerate = threshMod.Uint64() - - threshSev, err := arr[2].TryInteger() - if err != nil { - return err - } - c.VelocityThresholdSevere = threshSev.Uint64() - - threshExt, err := arr[3].TryInteger() - if err != nil { - return err - } - c.VelocityThresholdExtreme = threshExt.Uint64() - - rateMild, err := arr[4].TryInteger() - if err != nil { - return err - } - c.TributeRateMild = rateMild.Uint64() - - rateMod, err := arr[5].TryInteger() - if err != nil { - return err - } - c.TributeRateModerate = rateMod.Uint64() - - rateSev, err := arr[6].TryInteger() - if err != nil { - return err - } - c.TributeRateSevere = rateSev.Uint64() - - rateExt, err := arr[7].TryInteger() - if err != nil { - return err - } - c.TributeRateExtreme = rateExt.Uint64() - - incHigh, err := arr[8].TryInteger() - if err != nil { - return err - } - c.IncentiveRateHigh = incHigh.Uint64() - - incVeryHigh, err := arr[9].TryInteger() - if err != nil { - return err - } - c.IncentiveRateVeryHigh = incVeryHigh.Uint64() - - stagnancy, err := arr[10].TryInteger() - if err != nil { - return err - } - c.StagnancyPeriod = uint32(stagnancy.Uint64()) - - assessPeriod, err := arr[11].TryInteger() - if err != nil { - return err - } - c.AssessmentPeriod = uint32(assessPeriod.Uint64()) - - grace, err := arr[12].TryInteger() - if err != nil { - return err - } - c.GracePeriod = uint32(grace.Uint64()) - - minBal, err := arr[13].TryInteger() - if err != nil { - return err - } - c.MinBalanceForTribute = minBal.Uint64() - - exemptThresh, err := arr[14].TryInteger() - if err != nil { - return err - } - c.ExemptionThreshold = exemptThresh.Uint64() - - return nil -} +package state + +import ( + "errors" + "math/big" + + "github.com/tutus-one/tutus-chain/pkg/util" + "github.com/tutus-one/tutus-chain/pkg/vm/stackitem" +) + +// VelocityAccountStatus represents the velocity tracking account status. +type VelocityAccountStatus uint8 + +const ( + // VelocityAccountActive indicates the account is actively tracked. + VelocityAccountActive VelocityAccountStatus = 0 + // VelocityAccountExempt indicates the account is exempt from tribute. + VelocityAccountExempt VelocityAccountStatus = 1 + // VelocityAccountSuspended indicates velocity tracking is suspended. + VelocityAccountSuspended VelocityAccountStatus = 2 +) + +// AssessmentStatus represents the status of a tribute assessment. +type AssessmentStatus uint8 + +const ( + // AssessmentPending indicates the assessment is pending collection. + AssessmentPending AssessmentStatus = 0 + // AssessmentCollected indicates the tribute has been collected. + AssessmentCollected AssessmentStatus = 1 + // AssessmentWaived indicates the assessment was waived. + AssessmentWaived AssessmentStatus = 2 + // AssessmentAppealed indicates the assessment is under appeal. + AssessmentAppealed AssessmentStatus = 3 +) + +// HoardingLevel represents the severity of resource hoarding. +type HoardingLevel uint8 + +const ( + // HoardingNone indicates no hoarding detected. + HoardingNone HoardingLevel = 0 + // HoardingMild indicates mild hoarding (below normal velocity). + HoardingMild HoardingLevel = 1 + // HoardingModerate indicates moderate hoarding. + HoardingModerate HoardingLevel = 2 + // HoardingSevere indicates severe hoarding. + HoardingSevere HoardingLevel = 3 + // HoardingExtreme indicates extreme hoarding (stagnant resources). + HoardingExtreme HoardingLevel = 4 +) + +// IncentiveType represents types of circulation incentives. +type IncentiveType uint8 + +const ( + // IncentiveVelocityBonus rewards high resource velocity. + IncentiveVelocityBonus IncentiveType = 0 + // IncentiveProductiveUse rewards productive resource use. + IncentiveProductiveUse IncentiveType = 1 + // IncentiveCommunitySupport rewards community contributions. + IncentiveCommunitySupport IncentiveType = 2 + // IncentiveEducationSpending rewards education investment. + IncentiveEducationSpending IncentiveType = 3 + // IncentiveHealthcareSpending rewards healthcare investment. + IncentiveHealthcareSpending IncentiveType = 4 +) + +// VelocityAccount tracks resource velocity for a Vita holder. +type VelocityAccount struct { + VitaID uint64 // Owner's Vita token ID + Owner util.Uint160 // Owner's address + CurrentVelocity uint64 // Current velocity score (0-10000 basis points) + AverageVelocity uint64 // Rolling average velocity + LastActivityBlock uint32 // Last transaction block + TotalInflow uint64 // Total resources received + TotalOutflow uint64 // Total resources spent/transferred + StagnantBalance uint64 // Balance considered stagnant + HoardingLevel HoardingLevel // Current hoarding assessment + ExemptionReason string // Reason for exemption (if any) + TotalTributePaid uint64 // Lifetime tribute paid + TotalIncentivesRcvd uint64 // Lifetime incentives received + Status VelocityAccountStatus // Account status + CreatedAt uint32 // Block height when created + UpdatedAt uint32 // Last update block +} + +// ToStackItem implements stackitem.Convertible. +func (a *VelocityAccount) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.CurrentVelocity)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.AverageVelocity)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.LastActivityBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalInflow)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalOutflow)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantBalance)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), + stackitem.NewByteArray([]byte(a.ExemptionReason)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalTributePaid)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TotalIncentivesRcvd)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CreatedAt))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.UpdatedAt))), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (a *VelocityAccount) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 15 { + return errors.New("not a struct") + } + vitaID, err := arr[0].TryInteger() + if err != nil { + return err + } + a.VitaID = vitaID.Uint64() + + ownerBytes, err := arr[1].TryBytes() + if err != nil { + return err + } + a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) + if err != nil { + return err + } + + currentVel, err := arr[2].TryInteger() + if err != nil { + return err + } + a.CurrentVelocity = currentVel.Uint64() + + avgVel, err := arr[3].TryInteger() + if err != nil { + return err + } + a.AverageVelocity = avgVel.Uint64() + + lastActivity, err := arr[4].TryInteger() + if err != nil { + return err + } + a.LastActivityBlock = uint32(lastActivity.Uint64()) + + totalIn, err := arr[5].TryInteger() + if err != nil { + return err + } + a.TotalInflow = totalIn.Uint64() + + totalOut, err := arr[6].TryInteger() + if err != nil { + return err + } + a.TotalOutflow = totalOut.Uint64() + + stagnant, err := arr[7].TryInteger() + if err != nil { + return err + } + a.StagnantBalance = stagnant.Uint64() + + hoardLevel, err := arr[8].TryInteger() + if err != nil { + return err + } + a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) + + exemptReason, err := arr[9].TryBytes() + if err != nil { + return err + } + a.ExemptionReason = string(exemptReason) + + totalTrib, err := arr[10].TryInteger() + if err != nil { + return err + } + a.TotalTributePaid = totalTrib.Uint64() + + totalInc, err := arr[11].TryInteger() + if err != nil { + return err + } + a.TotalIncentivesRcvd = totalInc.Uint64() + + status, err := arr[12].TryInteger() + if err != nil { + return err + } + a.Status = VelocityAccountStatus(status.Uint64()) + + created, err := arr[13].TryInteger() + if err != nil { + return err + } + a.CreatedAt = uint32(created.Uint64()) + + updated, err := arr[14].TryInteger() + if err != nil { + return err + } + a.UpdatedAt = uint32(updated.Uint64()) + + return nil +} + +// TributeAssessment represents a hoarding tribute assessment. +type TributeAssessment struct { + ID uint64 // Unique assessment ID + VitaID uint64 // Owner's Vita ID + Owner util.Uint160 // Owner's address + AssessmentBlock uint32 // Block when assessment made + HoardingLevel HoardingLevel // Hoarding level at assessment + StagnantAmount uint64 // Amount considered stagnant + TributeRate uint64 // Rate applied (basis points) + TributeAmount uint64 // Tribute amount due + DueBlock uint32 // Block by which tribute is due + CollectedBlock uint32 // Block when collected (0 if not) + Status AssessmentStatus // Assessment status + AppealReason string // Reason for appeal (if any) +} + +// ToStackItem implements stackitem.Convertible. +func (a *TributeAssessment) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(a.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.VitaID)), + stackitem.NewByteArray(a.Owner.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.AssessmentBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.HoardingLevel))), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.StagnantAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeRate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(a.TributeAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.DueBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.CollectedBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(a.Status))), + stackitem.NewByteArray([]byte(a.AppealReason)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (a *TributeAssessment) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 12 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + a.ID = id.Uint64() + + vitaID, err := arr[1].TryInteger() + if err != nil { + return err + } + a.VitaID = vitaID.Uint64() + + ownerBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + a.Owner, err = util.Uint160DecodeBytesBE(ownerBytes) + if err != nil { + return err + } + + assessBlock, err := arr[3].TryInteger() + if err != nil { + return err + } + a.AssessmentBlock = uint32(assessBlock.Uint64()) + + hoardLevel, err := arr[4].TryInteger() + if err != nil { + return err + } + a.HoardingLevel = HoardingLevel(hoardLevel.Uint64()) + + stagnant, err := arr[5].TryInteger() + if err != nil { + return err + } + a.StagnantAmount = stagnant.Uint64() + + rate, err := arr[6].TryInteger() + if err != nil { + return err + } + a.TributeRate = rate.Uint64() + + amount, err := arr[7].TryInteger() + if err != nil { + return err + } + a.TributeAmount = amount.Uint64() + + due, err := arr[8].TryInteger() + if err != nil { + return err + } + a.DueBlock = uint32(due.Uint64()) + + collected, err := arr[9].TryInteger() + if err != nil { + return err + } + a.CollectedBlock = uint32(collected.Uint64()) + + status, err := arr[10].TryInteger() + if err != nil { + return err + } + a.Status = AssessmentStatus(status.Uint64()) + + appealReason, err := arr[11].TryBytes() + if err != nil { + return err + } + a.AppealReason = string(appealReason) + + return nil +} + +// CirculationIncentive represents a reward for resource circulation. +type CirculationIncentive struct { + ID uint64 // Unique incentive ID + VitaID uint64 // Recipient's Vita ID + Recipient util.Uint160 // Recipient's address + IncentiveType IncentiveType // Type of incentive + Amount uint64 // Incentive amount + Reason string // Reason for incentive + VelocityScore uint64 // Velocity score that triggered incentive + GrantedBlock uint32 // Block when granted + ClaimedBlock uint32 // Block when claimed (0 if not) + Claimed bool // Whether incentive was claimed +} + +// ToStackItem implements stackitem.Convertible. +func (i *CirculationIncentive) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(i.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.VitaID)), + stackitem.NewByteArray(i.Recipient.BytesBE()), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.IncentiveType))), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.Amount)), + stackitem.NewByteArray([]byte(i.Reason)), + stackitem.NewBigInteger(new(big.Int).SetUint64(i.VelocityScore)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.GrantedBlock))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(i.ClaimedBlock))), + stackitem.NewBool(i.Claimed), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (i *CirculationIncentive) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 10 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + i.ID = id.Uint64() + + vitaID, err := arr[1].TryInteger() + if err != nil { + return err + } + i.VitaID = vitaID.Uint64() + + recipBytes, err := arr[2].TryBytes() + if err != nil { + return err + } + i.Recipient, err = util.Uint160DecodeBytesBE(recipBytes) + if err != nil { + return err + } + + incType, err := arr[3].TryInteger() + if err != nil { + return err + } + i.IncentiveType = IncentiveType(incType.Uint64()) + + amount, err := arr[4].TryInteger() + if err != nil { + return err + } + i.Amount = amount.Uint64() + + reason, err := arr[5].TryBytes() + if err != nil { + return err + } + i.Reason = string(reason) + + velScore, err := arr[6].TryInteger() + if err != nil { + return err + } + i.VelocityScore = velScore.Uint64() + + granted, err := arr[7].TryInteger() + if err != nil { + return err + } + i.GrantedBlock = uint32(granted.Uint64()) + + claimed, err := arr[8].TryInteger() + if err != nil { + return err + } + i.ClaimedBlock = uint32(claimed.Uint64()) + + claimedBool, err := arr[9].TryBool() + if err != nil { + return err + } + i.Claimed = claimedBool + + return nil +} + +// RedistributionRecord tracks wealth redistribution events. +type RedistributionRecord struct { + ID uint64 // Unique record ID + SourceAssessment uint64 // Source assessment ID + TotalAmount uint64 // Total amount redistributed + RecipientCount uint64 // Number of recipients + PerCapitaAmount uint64 // Amount per recipient + RedistBlock uint32 // Block when redistribution occurred + TargetCategory string // Category of recipients (e.g., "low_velocity", "all_citizens") +} + +// ToStackItem implements stackitem.Convertible. +func (r *RedistributionRecord) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(r.ID)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.SourceAssessment)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.TotalAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.RecipientCount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(r.PerCapitaAmount)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(r.RedistBlock))), + stackitem.NewByteArray([]byte(r.TargetCategory)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (r *RedistributionRecord) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 7 { + return errors.New("not a struct") + } + + id, err := arr[0].TryInteger() + if err != nil { + return err + } + r.ID = id.Uint64() + + sourceAssess, err := arr[1].TryInteger() + if err != nil { + return err + } + r.SourceAssessment = sourceAssess.Uint64() + + total, err := arr[2].TryInteger() + if err != nil { + return err + } + r.TotalAmount = total.Uint64() + + recipCount, err := arr[3].TryInteger() + if err != nil { + return err + } + r.RecipientCount = recipCount.Uint64() + + perCapita, err := arr[4].TryInteger() + if err != nil { + return err + } + r.PerCapitaAmount = perCapita.Uint64() + + redistBlock, err := arr[5].TryInteger() + if err != nil { + return err + } + r.RedistBlock = uint32(redistBlock.Uint64()) + + targetCat, err := arr[6].TryBytes() + if err != nil { + return err + } + r.TargetCategory = string(targetCat) + + return nil +} + +// TributeConfig holds configurable parameters for the Tribute system. +type TributeConfig struct { + VelocityThresholdMild uint64 // Velocity below this = mild hoarding (basis points) + VelocityThresholdModerate uint64 // Velocity below this = moderate hoarding + VelocityThresholdSevere uint64 // Velocity below this = severe hoarding + VelocityThresholdExtreme uint64 // Velocity below this = extreme hoarding + TributeRateMild uint64 // Tribute rate for mild hoarding (basis points) + TributeRateModerate uint64 // Tribute rate for moderate hoarding + TributeRateSevere uint64 // Tribute rate for severe hoarding + TributeRateExtreme uint64 // Tribute rate for extreme hoarding + IncentiveRateHigh uint64 // Incentive rate for high velocity + IncentiveRateVeryHigh uint64 // Incentive rate for very high velocity + StagnancyPeriod uint32 // Blocks before balance considered stagnant + AssessmentPeriod uint32 // Blocks between assessments + GracePeriod uint32 // Blocks before tribute due after assessment + MinBalanceForTribute uint64 // Minimum balance to assess tribute + ExemptionThreshold uint64 // Balance below which exempt from tribute +} + +// ToStackItem implements stackitem.Convertible. +func (c *TributeConfig) ToStackItem() stackitem.Item { + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdMild)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdModerate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdSevere)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.VelocityThresholdExtreme)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateMild)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateModerate)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateSevere)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.TributeRateExtreme)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateHigh)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.IncentiveRateVeryHigh)), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.StagnancyPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.AssessmentPeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(uint64(c.GracePeriod))), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.MinBalanceForTribute)), + stackitem.NewBigInteger(new(big.Int).SetUint64(c.ExemptionThreshold)), + }) +} + +// FromStackItem implements stackitem.Convertible. +func (c *TributeConfig) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) < 15 { + return errors.New("not a struct") + } + + threshMild, err := arr[0].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdMild = threshMild.Uint64() + + threshMod, err := arr[1].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdModerate = threshMod.Uint64() + + threshSev, err := arr[2].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdSevere = threshSev.Uint64() + + threshExt, err := arr[3].TryInteger() + if err != nil { + return err + } + c.VelocityThresholdExtreme = threshExt.Uint64() + + rateMild, err := arr[4].TryInteger() + if err != nil { + return err + } + c.TributeRateMild = rateMild.Uint64() + + rateMod, err := arr[5].TryInteger() + if err != nil { + return err + } + c.TributeRateModerate = rateMod.Uint64() + + rateSev, err := arr[6].TryInteger() + if err != nil { + return err + } + c.TributeRateSevere = rateSev.Uint64() + + rateExt, err := arr[7].TryInteger() + if err != nil { + return err + } + c.TributeRateExtreme = rateExt.Uint64() + + incHigh, err := arr[8].TryInteger() + if err != nil { + return err + } + c.IncentiveRateHigh = incHigh.Uint64() + + incVeryHigh, err := arr[9].TryInteger() + if err != nil { + return err + } + c.IncentiveRateVeryHigh = incVeryHigh.Uint64() + + stagnancy, err := arr[10].TryInteger() + if err != nil { + return err + } + c.StagnancyPeriod = uint32(stagnancy.Uint64()) + + assessPeriod, err := arr[11].TryInteger() + if err != nil { + return err + } + c.AssessmentPeriod = uint32(assessPeriod.Uint64()) + + grace, err := arr[12].TryInteger() + if err != nil { + return err + } + c.GracePeriod = uint32(grace.Uint64()) + + minBal, err := arr[13].TryInteger() + if err != nil { + return err + } + c.MinBalanceForTribute = minBal.Uint64() + + exemptThresh, err := arr[14].TryInteger() + if err != nil { + return err + } + c.ExemptionThreshold = exemptThresh.Uint64() + + return nil +}