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",
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
HasRoleInternal(d *dao.Simple, address util.Uint160, roleID uint64, blockHeight uint32) bool
|
||||||
// HasPermissionInternal checks if address has permission via roles.
|
// HasPermissionInternal checks if address has permission via roles.
|
||||||
HasPermissionInternal(d *dao.Simple, address util.Uint160, resource, action string, scope state.Scope, blockHeight uint32) bool
|
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
|
// IVTS is an interface required from native VTS contract for
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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")
|
ErrRestrictionNotFound = errors.New("restriction not found")
|
||||||
ErrRestrictionExists = errors.New("restriction already exists")
|
ErrRestrictionExists = errors.New("restriction already exists")
|
||||||
ErrInvalidRightID = errors.New("invalid right ID")
|
ErrInvalidRightID = errors.New("invalid right ID")
|
||||||
ErrReasonTooLong = errors.New("reason too long")
|
|
||||||
ErrNoCaseID = errors.New("case ID required for due process")
|
ErrNoCaseID = errors.New("case ID required for due process")
|
||||||
ErrNoExpiration = errors.New("expiration required (no indefinite restrictions)")
|
ErrNoExpiration = errors.New("expiration required (no indefinite restrictions)")
|
||||||
ErrNotAuthorized = errors.New("not authorized")
|
ErrNotAuthorized = errors.New("not authorized")
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -1,130 +1,130 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/tutus-one/tutus-chain/pkg/util"
|
"github.com/tutus-one/tutus-chain/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgreementStatus represents the status of a bilateral agreement.
|
// AgreementStatus represents the status of a bilateral agreement.
|
||||||
type AgreementStatus uint8
|
type AgreementStatus uint8
|
||||||
|
|
||||||
// Agreement status constants.
|
// Agreement status constants.
|
||||||
const (
|
const (
|
||||||
AgreementPending AgreementStatus = 0
|
AgreementPending AgreementStatus = 0
|
||||||
AgreementActive AgreementStatus = 1
|
AgreementActive AgreementStatus = 1
|
||||||
AgreementSuspended AgreementStatus = 2
|
AgreementSuspended AgreementStatus = 2
|
||||||
AgreementTerminated AgreementStatus = 3
|
AgreementTerminated AgreementStatus = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgreementType represents the type of bilateral agreement.
|
// AgreementType represents the type of bilateral agreement.
|
||||||
type AgreementType uint8
|
type AgreementType uint8
|
||||||
|
|
||||||
// Agreement type constants.
|
// Agreement type constants.
|
||||||
const (
|
const (
|
||||||
AgreementTypeGeneral AgreementType = 0 // General cooperation
|
AgreementTypeGeneral AgreementType = 0 // General cooperation
|
||||||
AgreementTypeIdentity AgreementType = 1 // Identity verification
|
AgreementTypeIdentity AgreementType = 1 // Identity verification
|
||||||
AgreementTypeSettlement AgreementType = 2 // VTS settlement
|
AgreementTypeSettlement AgreementType = 2 // VTS settlement
|
||||||
AgreementTypeEducation AgreementType = 3 // Education credential sharing
|
AgreementTypeEducation AgreementType = 3 // Education credential sharing
|
||||||
AgreementTypeHealthcare AgreementType = 4 // Healthcare record sharing
|
AgreementTypeHealthcare AgreementType = 4 // Healthcare record sharing
|
||||||
AgreementTypeComprehensive AgreementType = 5 // All services
|
AgreementTypeComprehensive AgreementType = 5 // All services
|
||||||
)
|
)
|
||||||
|
|
||||||
// VerificationStatus represents the status of a verification request.
|
// VerificationStatus represents the status of a verification request.
|
||||||
type VerificationStatus uint8
|
type VerificationStatus uint8
|
||||||
|
|
||||||
// Verification status constants.
|
// Verification status constants.
|
||||||
const (
|
const (
|
||||||
VerificationPending VerificationStatus = 0
|
VerificationPending VerificationStatus = 0
|
||||||
VerificationApproved VerificationStatus = 1
|
VerificationApproved VerificationStatus = 1
|
||||||
VerificationRejected VerificationStatus = 2
|
VerificationRejected VerificationStatus = 2
|
||||||
VerificationExpired VerificationStatus = 3
|
VerificationExpired VerificationStatus = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// VerificationType represents the type of verification request.
|
// VerificationType represents the type of verification request.
|
||||||
type VerificationType uint8
|
type VerificationType uint8
|
||||||
|
|
||||||
// Verification type constants.
|
// Verification type constants.
|
||||||
const (
|
const (
|
||||||
VerificationTypeIdentity VerificationType = 0 // Identity verification
|
VerificationTypeIdentity VerificationType = 0 // Identity verification
|
||||||
VerificationTypeCredential VerificationType = 1 // Education credential
|
VerificationTypeCredential VerificationType = 1 // Education credential
|
||||||
VerificationTypeHealth VerificationType = 2 // Healthcare record
|
VerificationTypeHealth VerificationType = 2 // Healthcare record
|
||||||
VerificationTypeCertificate VerificationType = 3 // Professional certificate
|
VerificationTypeCertificate VerificationType = 3 // Professional certificate
|
||||||
)
|
)
|
||||||
|
|
||||||
// SettlementStatus represents the status of a settlement request.
|
// SettlementStatus represents the status of a settlement request.
|
||||||
type SettlementStatus uint8
|
type SettlementStatus uint8
|
||||||
|
|
||||||
// Settlement status constants.
|
// Settlement status constants.
|
||||||
const (
|
const (
|
||||||
SettlementPending SettlementStatus = 0
|
SettlementPending SettlementStatus = 0
|
||||||
SettlementCompleted SettlementStatus = 1
|
SettlementCompleted SettlementStatus = 1
|
||||||
SettlementRejected SettlementStatus = 2
|
SettlementRejected SettlementStatus = 2
|
||||||
SettlementCancelled SettlementStatus = 3
|
SettlementCancelled SettlementStatus = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// BilateralAgreement represents an agreement between two sovereign chains.
|
// BilateralAgreement represents an agreement between two sovereign chains.
|
||||||
type BilateralAgreement struct {
|
type BilateralAgreement struct {
|
||||||
ID uint64 // Unique agreement ID
|
ID uint64 // Unique agreement ID
|
||||||
LocalChainID uint32 // This chain's ID
|
LocalChainID uint32 // This chain's ID
|
||||||
RemoteChainID uint32 // Partner chain's ID
|
RemoteChainID uint32 // Partner chain's ID
|
||||||
AgreementType AgreementType // Type of agreement
|
AgreementType AgreementType // Type of agreement
|
||||||
Status AgreementStatus
|
Status AgreementStatus
|
||||||
Terms util.Uint256 // Hash of off-chain terms document
|
Terms util.Uint256 // Hash of off-chain terms document
|
||||||
EffectiveDate uint32 // Block height when effective
|
EffectiveDate uint32 // Block height when effective
|
||||||
ExpirationDate uint32 // Block height when expires (0 = no expiry)
|
ExpirationDate uint32 // Block height when expires (0 = no expiry)
|
||||||
CreatedAt uint32 // Block height when created
|
CreatedAt uint32 // Block height when created
|
||||||
UpdatedAt uint32 // Last update block height
|
UpdatedAt uint32 // Last update block height
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerificationRequest represents a cross-border verification request.
|
// VerificationRequest represents a cross-border verification request.
|
||||||
type VerificationRequest struct {
|
type VerificationRequest struct {
|
||||||
ID uint64 // Unique request ID
|
ID uint64 // Unique request ID
|
||||||
RequestingChain uint32 // Chain requesting verification
|
RequestingChain uint32 // Chain requesting verification
|
||||||
TargetChain uint32 // Chain being queried
|
TargetChain uint32 // Chain being queried
|
||||||
Subject util.Uint160 // Subject of verification
|
Subject util.Uint160 // Subject of verification
|
||||||
VerificationType VerificationType
|
VerificationType VerificationType
|
||||||
DataHash util.Uint256 // Hash of requested data
|
DataHash util.Uint256 // Hash of requested data
|
||||||
Status VerificationStatus
|
Status VerificationStatus
|
||||||
ResponseHash util.Uint256 // Hash of response data (if any)
|
ResponseHash util.Uint256 // Hash of response data (if any)
|
||||||
Requester util.Uint160 // Who initiated request
|
Requester util.Uint160 // Who initiated request
|
||||||
CreatedAt uint32 // Block height
|
CreatedAt uint32 // Block height
|
||||||
ExpiresAt uint32 // Request expiry
|
ExpiresAt uint32 // Request expiry
|
||||||
RespondedAt uint32 // When responded (0 = pending)
|
RespondedAt uint32 // When responded (0 = pending)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettlementRequest represents an international VTS settlement request.
|
// SettlementRequest represents an international VTS settlement request.
|
||||||
type SettlementRequest struct {
|
type SettlementRequest struct {
|
||||||
ID uint64 // Unique request ID
|
ID uint64 // Unique request ID
|
||||||
FromChain uint32 // Originating chain
|
FromChain uint32 // Originating chain
|
||||||
ToChain uint32 // Destination chain
|
ToChain uint32 // Destination chain
|
||||||
Sender util.Uint160 // Sender on from chain
|
Sender util.Uint160 // Sender on from chain
|
||||||
Receiver util.Uint160 // Receiver on to chain
|
Receiver util.Uint160 // Receiver on to chain
|
||||||
Amount uint64 // VTS amount (in smallest units)
|
Amount uint64 // VTS amount (in smallest units)
|
||||||
Reference string // Payment reference
|
Reference string // Payment reference
|
||||||
Status SettlementStatus
|
Status SettlementStatus
|
||||||
CreatedAt uint32 // Block height
|
CreatedAt uint32 // Block height
|
||||||
SettledAt uint32 // When settled (0 = pending)
|
SettledAt uint32 // When settled (0 = pending)
|
||||||
TxHash util.Uint256 // Settlement transaction hash
|
TxHash util.Uint256 // Settlement transaction hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// CredentialShare represents a shared credential between chains.
|
// CredentialShare represents a shared credential between chains.
|
||||||
type CredentialShare struct {
|
type CredentialShare struct {
|
||||||
ID uint64 // Unique share ID
|
ID uint64 // Unique share ID
|
||||||
SourceChain uint32 // Chain where credential originated
|
SourceChain uint32 // Chain where credential originated
|
||||||
TargetChain uint32 // Chain receiving credential
|
TargetChain uint32 // Chain receiving credential
|
||||||
Owner util.Uint160 // Credential owner
|
Owner util.Uint160 // Credential owner
|
||||||
CredentialType VerificationType
|
CredentialType VerificationType
|
||||||
CredentialID uint64 // Original credential ID on source chain
|
CredentialID uint64 // Original credential ID on source chain
|
||||||
ContentHash util.Uint256 // Hash of credential content
|
ContentHash util.Uint256 // Hash of credential content
|
||||||
ValidUntil uint32 // Validity period on target chain
|
ValidUntil uint32 // Validity period on target chain
|
||||||
CreatedAt uint32 // Block height
|
CreatedAt uint32 // Block height
|
||||||
IsRevoked bool // Has been revoked
|
IsRevoked bool // Has been revoked
|
||||||
}
|
}
|
||||||
|
|
||||||
// PonsConfig represents configurable parameters for the Pons contract.
|
// PonsConfig represents configurable parameters for the Pons contract.
|
||||||
type PonsConfig struct {
|
type PonsConfig struct {
|
||||||
LocalChainID uint32 // This chain's unique identifier
|
LocalChainID uint32 // This chain's unique identifier
|
||||||
VerificationTimeout uint32 // Blocks until verification request expires
|
VerificationTimeout uint32 // Blocks until verification request expires
|
||||||
SettlementTimeout uint32 // Blocks until settlement request expires
|
SettlementTimeout uint32 // Blocks until settlement request expires
|
||||||
MaxPendingRequests uint64 // Maximum pending requests per chain
|
MaxPendingRequests uint64 // Maximum pending requests per chain
|
||||||
CredentialShareExpiry uint32 // Default validity period for shared credentials
|
CredentialShareExpiry uint32 // Default validity period for shared credentials
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue