Add security remediation infrastructure and fixes
Implement comprehensive security infrastructure from Security-Remediation-Plan.md: New Infrastructure Files: - circuit_breaker.go (ARCH-001): Circuit breaker pattern for contract failures with Closed/Open/Half-Open state machine and emergency shutdown - invariants.go (ARCH-003): Formal verification with 14 critical invariants and TLA+ specifications for runtime verification - canary_deployment.go (ARCH-004): Feature flags with percentage-based rollout and auto-rollback on error rate threshold - audit_logger.go (ARCH-005): Comprehensive audit logging with multi-index querying across 8 categories (Auth, Access, Data, Governance, etc.) - config_registry.go (LOW-003): Configuration governance infrastructure - event_archival.go (LOW-001): Event log archival with Merkle commitments - validation.go (LOW-002): Common input validation constants and utilities - role_registry_domain.go (CRIT-002): Domain-specific committee roles (Legal, Health, Education, Economy, Identity, Governance) Contract Security Fixes: - contract.go: Add HasDomainCommitteeAuthority to IRoleRegistry interface - lex.go: Remove duplicate ErrReasonTooLong declaration - salus.go, eligere.go, palam.go, pons.go, scire.go, tribute.go: Line ending normalization and security validation integration - collocatio.go: Security validation for investment operations State Type Updates: - state/pons.go, state/salus.go, state/tribute.go: Line ending fixes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6bf48ea9d2
commit
1b4a61fcf7
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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",
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue