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

505 lines
14 KiB
Go

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