tutus-chain/pkg/core/native/circuit_breaker.go

556 lines
16 KiB
Go

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",
}