package native import ( "encoding/binary" "git.marketally.com/tutus-one/tutus-chain/pkg/core/dao" "git.marketally.com/tutus-one/tutus-chain/pkg/core/storage" "git.marketally.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", }