556 lines
16 KiB
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",
|
|
}
|