505 lines
14 KiB
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",
|
|
}
|