mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
427 lines
11 KiB
Go
427 lines
11 KiB
Go
// Copyright (C) 2025, Lux Industries Inc All rights reserved.
|
|
// Practical AI Models - Simple, Effective, No Bullshit
|
|
|
|
package ai
|
|
|
|
import (
|
|
"context"
|
|
cryptorand "crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// SimpleModel is a basic but effective AI model for consensus decisions
|
|
type SimpleModel[T ConsensusData] struct {
|
|
weights map[string]float64 // feature -> weight
|
|
bias float64
|
|
learningRate float64
|
|
features FeatureExtractor[T]
|
|
history []TrainingExample[T]
|
|
nodeID string
|
|
}
|
|
|
|
// FeatureExtractor converts consensus data to features for ML
|
|
type FeatureExtractor[T ConsensusData] interface {
|
|
Extract(data T) map[string]float64
|
|
Names() []string
|
|
}
|
|
|
|
// NewSimpleModel creates a practical AI model
|
|
func NewSimpleModel[T ConsensusData](nodeID string, extractor FeatureExtractor[T]) *SimpleModel[T] {
|
|
return &SimpleModel[T]{
|
|
weights: make(map[string]float64),
|
|
bias: 0.0,
|
|
learningRate: 0.01,
|
|
features: extractor,
|
|
history: make([]TrainingExample[T], 0),
|
|
nodeID: nodeID,
|
|
}
|
|
}
|
|
|
|
// Decide makes a decision based on current model state
|
|
func (m *SimpleModel[T]) Decide(ctx context.Context, input T, context map[string]interface{}) (*Decision[T], error) {
|
|
features := m.features.Extract(input)
|
|
|
|
// Simple linear decision function
|
|
score := m.bias
|
|
for feature, value := range features {
|
|
weight := m.weights[feature]
|
|
score += weight * value
|
|
}
|
|
|
|
// Convert to probability
|
|
confidence := sigmoid(score)
|
|
|
|
// Determine action based on score
|
|
action := "approve"
|
|
if score < 0 {
|
|
action = "reject"
|
|
}
|
|
|
|
// Generate reasoning
|
|
reasoning := m.generateReasoning(features, score, action)
|
|
|
|
decision := &Decision[T]{
|
|
ID: generateID(),
|
|
Action: action,
|
|
Data: input,
|
|
Confidence: confidence,
|
|
Reasoning: reasoning,
|
|
Context: context,
|
|
Timestamp: time.Now(),
|
|
ProposerID: m.nodeID,
|
|
}
|
|
|
|
return decision, nil
|
|
}
|
|
|
|
// ProposeDecision creates a proposal for consensus
|
|
func (m *SimpleModel[T]) ProposeDecision(ctx context.Context, input T) (*Proposal[T], error) {
|
|
decision, err := m.Decide(ctx, input, make(map[string]interface{}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proposal := &Proposal[T]{
|
|
ID: generateID(),
|
|
NodeID: m.nodeID,
|
|
Decision: decision,
|
|
Evidence: []Evidence[T]{{
|
|
Data: input,
|
|
NodeID: m.nodeID,
|
|
Weight: 1.0,
|
|
Timestamp: time.Now(),
|
|
}},
|
|
Weight: 1.0,
|
|
Confidence: decision.Confidence,
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
return proposal, nil
|
|
}
|
|
|
|
// ValidateProposal validates another node's proposal
|
|
func (m *SimpleModel[T]) ValidateProposal(proposal *Proposal[T]) (float64, error) {
|
|
if proposal.Decision == nil {
|
|
return 0.0, fmt.Errorf("proposal has no decision")
|
|
}
|
|
|
|
// Re-evaluate the same input
|
|
ourDecision, err := m.Decide(context.Background(), proposal.Decision.Data, proposal.Decision.Context)
|
|
if err != nil {
|
|
return 0.0, err
|
|
}
|
|
|
|
// Compare decisions
|
|
agreement := 0.0
|
|
if ourDecision.Action == proposal.Decision.Action {
|
|
agreement = 1.0
|
|
}
|
|
|
|
// Weight by confidence similarity
|
|
confidenceDiff := math.Abs(ourDecision.Confidence - proposal.Decision.Confidence)
|
|
confidenceWeight := 1.0 - confidenceDiff
|
|
|
|
// Combined confidence
|
|
validation := agreement * confidenceWeight * ourDecision.Confidence
|
|
|
|
return validation, nil
|
|
}
|
|
|
|
// Learn updates the model with training examples
|
|
func (m *SimpleModel[T]) Learn(examples []TrainingExample[T]) error {
|
|
for _, example := range examples {
|
|
if err := m.learnExample(example); err != nil {
|
|
return fmt.Errorf("learning failed for example: %w", err)
|
|
}
|
|
}
|
|
|
|
m.history = append(m.history, examples...)
|
|
|
|
// Keep history bounded
|
|
if len(m.history) > 10000 {
|
|
m.history = m.history[len(m.history)-10000:]
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateWeights applies gradient updates
|
|
func (m *SimpleModel[T]) UpdateWeights(gradients []float64) error {
|
|
featureNames := m.features.Names()
|
|
|
|
if len(gradients) != len(featureNames)+1 { // +1 for bias
|
|
return fmt.Errorf("gradient size mismatch: got %d, expected %d", len(gradients), len(featureNames)+1)
|
|
}
|
|
|
|
// Update feature weights
|
|
for i, feature := range featureNames {
|
|
m.weights[feature] -= m.learningRate * gradients[i]
|
|
}
|
|
|
|
// Update bias
|
|
m.bias -= m.learningRate * gradients[len(featureNames)]
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetState returns current model state
|
|
func (m *SimpleModel[T]) GetState() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"weights": m.weights,
|
|
"bias": m.bias,
|
|
"learning_rate": m.learningRate,
|
|
"node_id": m.nodeID,
|
|
"history_size": len(m.history),
|
|
"last_update": time.Now(),
|
|
}
|
|
}
|
|
|
|
// LoadState loads model state
|
|
func (m *SimpleModel[T]) LoadState(state map[string]interface{}) error {
|
|
if weights, ok := state["weights"].(map[string]interface{}); ok {
|
|
m.weights = make(map[string]float64)
|
|
for k, v := range weights {
|
|
if val, ok := v.(float64); ok {
|
|
m.weights[k] = val
|
|
}
|
|
}
|
|
}
|
|
|
|
if bias, ok := state["bias"].(float64); ok {
|
|
m.bias = bias
|
|
}
|
|
|
|
if lr, ok := state["learning_rate"].(float64); ok {
|
|
m.learningRate = lr
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Private methods
|
|
|
|
func (m *SimpleModel[T]) learnExample(example TrainingExample[T]) error {
|
|
features := m.features.Extract(example.Input)
|
|
|
|
// Current prediction
|
|
score := m.bias
|
|
for feature, value := range features {
|
|
score += m.weights[feature] * value
|
|
}
|
|
|
|
prediction := sigmoid(score)
|
|
|
|
// Target based on feedback
|
|
target := (example.Feedback + 1.0) / 2.0 // convert -1,1 to 0,1
|
|
|
|
// Error
|
|
error := target - prediction
|
|
|
|
// Gradient descent update
|
|
for feature, value := range features {
|
|
gradient := error * prediction * (1 - prediction) * value * example.Weight
|
|
m.weights[feature] += m.learningRate * gradient
|
|
}
|
|
|
|
// Update bias
|
|
biasGradient := error * prediction * (1 - prediction) * example.Weight
|
|
m.bias += m.learningRate * biasGradient
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *SimpleModel[T]) generateReasoning(features map[string]float64, score float64, action string) string {
|
|
// Find most influential features
|
|
topFeatures := make([]string, 0)
|
|
for feature, value := range features {
|
|
weight := m.weights[feature]
|
|
influence := math.Abs(weight * value)
|
|
|
|
if influence > 0.1 { // threshold for significance
|
|
topFeatures = append(topFeatures, fmt.Sprintf("%s(%.2f)", feature, influence))
|
|
}
|
|
}
|
|
|
|
reasoning := fmt.Sprintf("Decision to %s based on score %.3f. Key factors: %v",
|
|
action, score, topFeatures)
|
|
|
|
return reasoning
|
|
}
|
|
|
|
// === FEATURE EXTRACTORS ===
|
|
|
|
// BlockFeatureExtractor extracts features from block data
|
|
type BlockFeatureExtractor struct{}
|
|
|
|
func (e *BlockFeatureExtractor) Extract(data BlockData) map[string]float64 {
|
|
now := time.Now()
|
|
age := now.Sub(data.Timestamp).Seconds()
|
|
|
|
return map[string]float64{
|
|
"height": float64(data.Height),
|
|
"tx_count": float64(len(data.Transactions)),
|
|
"age_seconds": age,
|
|
"age_normalized": sigmoid(age / 3600), // normalize by hour
|
|
"hash_complexity": hashComplexity(data.Hash),
|
|
"time_since_last": float64(now.Unix() - data.Timestamp.Unix()),
|
|
}
|
|
}
|
|
|
|
func (e *BlockFeatureExtractor) Names() []string {
|
|
return []string{"height", "tx_count", "age_seconds", "age_normalized", "hash_complexity", "time_since_last"}
|
|
}
|
|
|
|
// TransactionFeatureExtractor extracts features from transaction data
|
|
type TransactionFeatureExtractor struct{}
|
|
|
|
func (e *TransactionFeatureExtractor) Extract(data TransactionData) map[string]float64 {
|
|
return map[string]float64{
|
|
"amount": float64(data.Amount),
|
|
"fee": float64(data.Fee),
|
|
"fee_ratio": float64(data.Fee) / math.Max(float64(data.Amount), 1),
|
|
"data_size": float64(len(fmt.Sprintf("%v", data.Data))),
|
|
"age_seconds": time.Since(data.Timestamp).Seconds(),
|
|
"from_entropy": addressEntropy(data.From),
|
|
"to_entropy": addressEntropy(data.To),
|
|
}
|
|
}
|
|
|
|
func (e *TransactionFeatureExtractor) Names() []string {
|
|
return []string{"amount", "fee", "fee_ratio", "data_size", "age_seconds", "from_entropy", "to_entropy"}
|
|
}
|
|
|
|
// UpgradeFeatureExtractor extracts features from upgrade data
|
|
type UpgradeFeatureExtractor struct{}
|
|
|
|
func (e *UpgradeFeatureExtractor) Extract(data UpgradeData) map[string]float64 {
|
|
riskScore := 0.0
|
|
switch data.Risk {
|
|
case "low":
|
|
riskScore = 0.25
|
|
case "medium":
|
|
riskScore = 0.5
|
|
case "high":
|
|
riskScore = 0.75
|
|
case "critical":
|
|
riskScore = 1.0
|
|
}
|
|
|
|
return map[string]float64{
|
|
"change_count": float64(len(data.Changes)),
|
|
"risk_score": riskScore,
|
|
"test_count": float64(len(data.TestResults)),
|
|
"age_hours": time.Since(data.Timestamp).Hours(),
|
|
"version_entropy": versionEntropy(data.Version),
|
|
}
|
|
}
|
|
|
|
func (e *UpgradeFeatureExtractor) Names() []string {
|
|
return []string{"change_count", "risk_score", "test_count", "age_hours", "version_entropy"}
|
|
}
|
|
|
|
// === UTILITY FUNCTIONS ===
|
|
|
|
func sigmoid(x float64) float64 {
|
|
return 1.0 / (1.0 + math.Exp(-x))
|
|
}
|
|
|
|
func generateID() string {
|
|
h := sha256.New()
|
|
// Use crypto/rand for secure random bytes
|
|
randomBytes := make([]byte, 8)
|
|
_, _ = cryptorand.Read(randomBytes)
|
|
h.Write([]byte(fmt.Sprintf("%d_%x", time.Now().UnixNano(), randomBytes)))
|
|
return fmt.Sprintf("%x", h.Sum(nil))[:16]
|
|
}
|
|
|
|
func hashComplexity(hash string) float64 {
|
|
if len(hash) == 0 {
|
|
return 0
|
|
}
|
|
|
|
complexity := 0.0
|
|
for _, char := range hash {
|
|
if char >= '0' && char <= '9' {
|
|
complexity += 0.1
|
|
} else if char >= 'a' && char <= 'f' {
|
|
complexity += 0.2
|
|
}
|
|
}
|
|
|
|
return complexity / float64(len(hash))
|
|
}
|
|
|
|
func addressEntropy(address string) float64 {
|
|
if len(address) == 0 {
|
|
return 0
|
|
}
|
|
|
|
charCount := make(map[rune]int)
|
|
for _, char := range address {
|
|
charCount[char]++
|
|
}
|
|
|
|
entropy := 0.0
|
|
length := float64(len(address))
|
|
|
|
for _, count := range charCount {
|
|
if count > 0 {
|
|
p := float64(count) / length
|
|
entropy -= p * math.Log2(p)
|
|
}
|
|
}
|
|
|
|
return entropy
|
|
}
|
|
|
|
func versionEntropy(version string) float64 {
|
|
// Simple version entropy based on semantic versioning patterns
|
|
if len(version) == 0 {
|
|
return 0
|
|
}
|
|
|
|
entropy := 0.0
|
|
for i, char := range version {
|
|
if char == '.' {
|
|
entropy += 0.5
|
|
} else if char >= '0' && char <= '9' {
|
|
entropy += 0.1 * float64(i)
|
|
}
|
|
}
|
|
|
|
return entropy / float64(len(version))
|
|
}
|
|
|
|
// GetWeights returns the current model weights
|
|
func (m *SimpleModel[T]) GetWeights() map[string]float64 {
|
|
weights := make(map[string]float64, len(m.weights))
|
|
for k, v := range m.weights {
|
|
weights[k] = v
|
|
}
|
|
return weights
|
|
}
|
|
|
|
// SetWeights updates the model weights
|
|
func (m *SimpleModel[T]) SetWeights(weights map[string]float64) {
|
|
m.weights = make(map[string]float64, len(weights))
|
|
for k, v := range weights {
|
|
m.weights[k] = v
|
|
}
|
|
}
|
|
|
|
// Factory functions for feature extractors
|
|
func NewBlockFeatures() *BlockFeatureExtractor {
|
|
return &BlockFeatureExtractor{}
|
|
}
|
|
|
|
func NewTransactionFeatures() *TransactionFeatureExtractor {
|
|
return &TransactionFeatureExtractor{}
|
|
}
|
|
|
|
func NewUpgradeFeatures() *UpgradeFeatureExtractor {
|
|
return &UpgradeFeatureExtractor{}
|
|
}
|