Files
sonr/x/dwn/client/plugin/manager.go
T
40eadc995e Feat/1285 es ucan formatting (#1302)
* feat: Add Enclave Usage Examples

* feat(es/ucan): Add comprehensive integration tests

- Create integration.test.ts with full UCAN token lifecycle testing
- Cover end-to-end token creation, parsing, and validation
- Test capability attenuation and delegation chains
- Validate multi-algorithm support and timestamp scenarios
- Implement error recovery and performance test scenarios

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* chore: Remove migrated components and add migration documentation

Removed all code and references for components that have been moved to separate repositories:

**Moved to sonr-io/hway:**
- bridge/ - HTTP service with OAuth2/OIDC/WebAuthn handlers
- cmd/hway/ - Highway service binary
- internal/migrations/ - PostgreSQL schema migrations

**Moved to sonr-io/motr:**
- cmd/motr/ - Motor worker service (WASM vault operations)
- cmd/vault/ - Vault CLI tool
- crypto/ - Comprehensive cryptographic library
- packages/ - TypeScript SDK packages (es, sdk, ui, com, pkl)
- web/auth/ - Authentication web application
- web/dash/ - Dashboard web application

**Updated Configuration:**
- Makefile: Removed build/test/release targets for moved components
- CLAUDE.md: Simplified to focus on core blockchain components
- devbox.json: Removed scripts for moved services
- docker-compose.yml: Removed hway, postgres, redis, auth, dash services
- .github/scopes.yml: Removed CI scopes for migrated components
- .goreleaser.yml: Updated release configuration

**Added Migration Documentation:**
- MIGRATE_HWAY.md: Comprehensive Highway service architecture and migration guide
- MIGRATE_MOTR.md: Comprehensive Motor/Worker/Vault architecture and migration guide

These migration documents provide complete context for setting up the new repositories including architecture diagrams, component breakdowns, API documentation, and migration checklists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* chore: Remove contracts references and documentation

Removed all references to the contracts directory that was migrated to a separate repository.

**Changes:**
- .gitignore: Removed contract-specific ignore patterns for DAO and wSNR contracts
- .gitignore: Removed hway and motr binary references (already migrated)
- .rgignore: Removed contracts, chains, and crypto directory references
- docs/reference/contracts/: Removed DAO.mdx and wSNR.mdx documentation files

This completes the cleanup of migrated components from the repository.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add crypto library migration documentation

Added comprehensive migration documentation for the crypto library that was
moved to sonr-io/crypto repository. This documentation provides complete context
for understanding the cryptographic primitives and protocols used throughout
the Sonr ecosystem.

## Key Documentation Added

### MIGRATE_CRYPTO.md
Complete documentation of the crypto library covering:

**Core Cryptographic Primitives**
- Elliptic curve implementations (Ed25519, Secp256k1, P-256, BLS12-381, Pallas/Vesta)
- Native curve arithmetic with optimized field operations
- Pairing-friendly curves for BLS signatures

**Multi-Party Computation (MPC)**
- MPC enclave for vault key generation and management
- Threshold cryptography (TECDSA, TED25519 with FROST protocol)
- Distributed Key Generation (DKG) via Gennaro and FROST protocols
- Secret sharing schemes (Shamir, Feldman VSS, Pedersen VSS)

**Digital Signature Schemes**
- BLS signatures with aggregation support
- BBS+ signatures for selective disclosure
- Schnorr signatures (standard and Mina/NEM variants)
- ECDSA with deterministic nonce generation

**Zero-Knowledge Proofs**
- Bulletproofs for range proofs
- Inner Product Arguments (IPA)
- Batch verification support

**Advanced Cryptographic Protocols**
- Cryptographic accumulators for set membership proofs
- Paillier homomorphic encryption
- Oblivious Transfer (OT) protocols
- Verifiable Random Functions (VRF)

**Key Management & Identity**
- DID key management with multi-chain support
- Multi-algorithm public key handling
- Wallet address derivation (Bitcoin, Ethereum, Cosmos, Solana, etc.)

**UCAN Integration**
- User-Controlled Authorization Networks
- Capability delegation and attenuation
- JWT-based capability tokens
- MPC-enabled UCAN signing

**Security Utilities**
- AEAD encryption (AES-GCM, AES-SIV)
- Argon2 key derivation
- ECIES encryption
- Secure memory handling

### MIGRATE_MOTR.md Updates
Updated Motor migration documentation to clarify that the crypto library
is now a separate external dependency at github.com/sonr-io/crypto v1.0.1

## Repository Context

The crypto library has been successfully migrated to its own repository
and is published as a Go module. It serves as the foundational cryptographic
layer for:
- Sonr blockchain (snrd) - DID signatures, vault operations
- Highway service (hway) - UCAN token signing, WebAuthn
- Motor/Worker (motr) - MPC vault operations, threshold signatures

## Integration Impact

All Sonr ecosystem components now depend on the external crypto library:
```go
require github.com/sonr-io/crypto v1.0.1
```

The migration enables independent versioning and maintenance of cryptographic
primitives while maintaining security and compatibility across the ecosystem.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* No commit suggestions generated

* No commit suggestions generated

* No commit suggestions generated

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-10 11:47:18 -04:00

500 lines
13 KiB
Go

package plugin
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
extism "github.com/extism/go-sdk"
"github.com/sonr-io/crypto/mpc"
"github.com/sonr-io/crypto/wasm"
)
// Manager handles the lifecycle of Motor plugin instances with health monitoring,
// automatic recovery, and efficient resource management.
type Manager struct {
mu sync.RWMutex
plugins map[string]*PluginState
loaderConfig *LoaderConfig
// Cleanup configuration
cleanupInterval time.Duration
maxIdleTime time.Duration
// Background cleanup goroutine
stopCleanup chan struct{}
cleanupWG sync.WaitGroup
}
// NewManager creates a new plugin manager with the specified configuration.
func NewManager(loaderConfig *LoaderConfig) *Manager {
if loaderConfig == nil {
loaderConfig = DefaultLoaderConfig()
}
m := &Manager{
plugins: make(map[string]*PluginState),
loaderConfig: loaderConfig,
cleanupInterval: 5 * time.Minute,
maxIdleTime: 30 * time.Minute,
stopCleanup: make(chan struct{}),
}
// Start background cleanup goroutine
m.cleanupWG.Add(1)
go m.cleanupLoop()
return m
}
// LoadPlugin loads a Motor plugin with the specified enclave configuration.
// Returns a cached instance if available, or creates a new one.
func (m *Manager) LoadPlugin(ctx context.Context, config *EnclaveConfig) (Plugin, error) {
// Validate configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid enclave configuration: %w", err)
}
// Generate plugin ID based on configuration
pluginID := m.generatePluginID(config)
m.mu.RLock()
state, exists := m.plugins[pluginID]
m.mu.RUnlock()
// Check if we have a healthy cached instance
if exists && state.IsHealthy && !state.IsExpired(m.maxIdleTime) {
state.UpdateHealth(nil) // Update last used timestamp
return &managedPluginImpl{
state: state,
manager: m,
}, nil
}
// Create new plugin instance
return m.createPlugin(ctx, pluginID, config)
}
// LoadPluginWithID loads a plugin with a specific ID for testing or debugging.
func (m *Manager) LoadPluginWithID(
ctx context.Context,
id string,
config *EnclaveConfig,
) (Plugin, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid enclave configuration: %w", err)
}
return m.createPlugin(ctx, id, config)
}
// createPlugin creates a new plugin instance with the given configuration.
func (m *Manager) createPlugin(
ctx context.Context,
id string,
config *EnclaveConfig,
) (Plugin, error) {
m.mu.Lock()
defer m.mu.Unlock()
// Check concurrent plugin limit
if len(m.plugins) >= m.loaderConfig.MaxConcurrentPlugins {
return nil, fmt.Errorf(
"maximum concurrent plugins limit reached (%d)",
m.loaderConfig.MaxConcurrentPlugins,
)
}
// Verify WASM integrity before loading
if err := VerifyPluginIntegrity(motrPluginBytes); err != nil {
return nil, fmt.Errorf("WASM integrity verification failed: %w", err)
}
// Verify signature if manifest is available (optional for now)
if manifest := GetPluginSignatureManifest(); manifest != nil {
if err := wasm.VerifyWithManifest(motrPluginBytes, manifest); err != nil {
// Log warning but don't fail for backward compatibility
// In production, this should return an error
// Using fmt.Printf as we don't have access to pdk here
fmt.Printf("WARNING: WASM signature verification failed: %v\n", err)
}
}
// Convert configuration to manifest format
manifestConfig, err := config.ToManifestConfig()
if err != nil {
return nil, fmt.Errorf("failed to convert config to manifest: %w", err)
}
// Create Extism manifest
manifest := extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: manifestConfig,
}
// Create plugin with timeout
pluginConfig := m.loaderConfig.ToPluginConfig()
// Create context with timeout for plugin initialization
initCtx, cancel := context.WithTimeout(ctx, config.Timeouts.PluginInit)
defer cancel()
plugin, err := extism.NewPlugin(initCtx, manifest, pluginConfig, []extism.HostFunction{})
if err != nil {
return nil, fmt.Errorf("failed to create plugin: %w", err)
}
// Create plugin state
state := NewPluginState(id, config, plugin)
m.plugins[id] = state
return &managedPluginImpl{
state: state,
manager: m,
}, nil
}
// RecoverPlugin attempts to recover a failed plugin instance.
func (m *Manager) RecoverPlugin(ctx context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
state, exists := m.plugins[id]
if !exists {
return fmt.Errorf("plugin %s not found", id)
}
// Close existing plugin
if state.Plugin != nil {
state.Plugin.Close(ctx)
}
// Recreate plugin with same configuration
manifestConfig, err := state.Config.ToManifestConfig()
if err != nil {
return fmt.Errorf("failed to convert config to manifest: %w", err)
}
manifest := extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: manifestConfig,
}
pluginConfig := m.loaderConfig.ToPluginConfig()
initCtx, cancel := context.WithTimeout(ctx, state.Config.Timeouts.PluginInit)
defer cancel()
plugin, err := extism.NewPlugin(initCtx, manifest, pluginConfig, []extism.HostFunction{})
if err != nil {
return fmt.Errorf("failed to recover plugin: %w", err)
}
// Update state
state.Plugin = plugin
state.IsHealthy = true
state.ErrorCount = 0
state.LastUsed = time.Now()
return nil
}
// GetPluginStats returns statistics for a specific plugin.
func (m *Manager) GetPluginStats(id string) (*PluginStats, error) {
m.mu.RLock()
defer m.mu.RUnlock()
state, exists := m.plugins[id]
if !exists {
return nil, fmt.Errorf("plugin %s not found", id)
}
return &PluginStats{
ID: state.ID,
CreatedAt: state.CreatedAt,
LastUsed: state.LastUsed,
IsHealthy: state.IsHealthy,
ErrorCount: state.ErrorCount,
ChainID: state.Config.ChainID,
UptimeDuration: time.Since(state.CreatedAt),
IdleDuration: time.Since(state.LastUsed),
}, nil
}
// ListPlugins returns a list of all currently managed plugins.
func (m *Manager) ListPlugins() []string {
m.mu.RLock()
defer m.mu.RUnlock()
ids := make([]string, 0, len(m.plugins))
for id := range m.plugins {
ids = append(ids, id)
}
return ids
}
// ClosePlugin closes and removes a specific plugin instance.
func (m *Manager) ClosePlugin(id string) error {
m.mu.Lock()
defer m.mu.Unlock()
state, exists := m.plugins[id]
if !exists {
return fmt.Errorf("plugin %s not found", id)
}
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
delete(m.plugins, id)
return nil
}
// Close shuts down the manager and all managed plugins.
func (m *Manager) Close() error {
// Stop cleanup goroutine
close(m.stopCleanup)
m.cleanupWG.Wait()
m.mu.Lock()
defer m.mu.Unlock()
// Close all plugins
for id, state := range m.plugins {
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
delete(m.plugins, id)
}
return nil
}
// generatePluginID generates a unique plugin ID based on configuration.
func (m *Manager) generatePluginID(config *EnclaveConfig) string {
// Use chain ID and enclave data hash for unique identification
if config.EnclaveData != nil && len(config.EnclaveData.PubBytes) > 8 {
pubKeyHash := fmt.Sprintf("%x", config.EnclaveData.PubBytes[:8])
return fmt.Sprintf("%s_%s", config.ChainID, pubKeyHash)
}
return fmt.Sprintf("%s_%d", config.ChainID, time.Now().UnixNano())
}
// cleanupLoop runs periodic cleanup of expired and unhealthy plugins.
func (m *Manager) cleanupLoop() {
defer m.cleanupWG.Done()
ticker := time.NewTicker(m.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.cleanupExpiredPlugins()
case <-m.stopCleanup:
return
}
}
}
// cleanupExpiredPlugins removes expired and unhealthy plugin instances.
func (m *Manager) cleanupExpiredPlugins() {
m.mu.Lock()
defer m.mu.Unlock()
var toRemove []string
for id, state := range m.plugins {
if !state.IsHealthy || state.IsExpired(m.maxIdleTime) {
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
toRemove = append(toRemove, id)
}
}
for _, id := range toRemove {
delete(m.plugins, id)
}
}
// PluginStats contains statistics and status information for a plugin instance.
type PluginStats struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
LastUsed time.Time `json:"last_used"`
IsHealthy bool `json:"is_healthy"`
ErrorCount int `json:"error_count"`
ChainID string `json:"chain_id"`
UptimeDuration time.Duration `json:"uptime_duration"`
IdleDuration time.Duration `json:"idle_duration"`
}
// managedPluginImpl implements the Plugin interface with health monitoring.
type managedPluginImpl struct {
state *PluginState
manager *Manager
}
// UCAN Token Operations with health monitoring
// callTokenMethod is a helper method to reduce duplication between token creation methods
func (p *managedPluginImpl) callTokenMethod(
methodName string,
request any,
) (*UCANTokenResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.TokenCreation)
defer cancel()
reqBytes, err := json.Marshal(request)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, methodName, reqBytes)
if err != nil {
p.state.UpdateHealth(err)
// Attempt recovery on failure
if !p.state.IsHealthy {
if recoverErr := p.manager.RecoverPlugin(ctx, p.state.ID); recoverErr == nil {
// Retry after recovery
_, r, err = p.state.Plugin.CallWithContext(ctx, methodName, reqBytes)
}
}
if err != nil {
return nil, err
}
}
var resp UCANTokenResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// NewOriginToken creates a new origin UCAN token with health monitoring and recovery.
func (p *managedPluginImpl) NewOriginToken(req *NewOriginTokenRequest) (*UCANTokenResponse, error) {
return p.callTokenMethod("new_origin_token", req)
}
func (p *managedPluginImpl) NewAttenuatedToken(
req *NewAttenuatedTokenRequest,
) (*UCANTokenResponse, error) {
return p.callTokenMethod("new_attenuated_token", req)
}
// Cryptographic Operations with health monitoring
func (p *managedPluginImpl) SignData(req *SignDataRequest) (*SignDataResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.Signature)
defer cancel()
reqBytes, err := json.Marshal(req)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, "sign_data", reqBytes)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp SignDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
func (p *managedPluginImpl) VerifyData(req *VerifyDataRequest) (*VerifyDataResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.Verification)
defer cancel()
reqBytes, err := json.Marshal(req)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, "verify_data", reqBytes)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp VerifyDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// Identity Operations with health monitoring
func (p *managedPluginImpl) GetIssuerDID() (*GetIssuerDIDResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, r, err := p.state.Plugin.CallWithContext(ctx, "get_issuer_did", []byte{})
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp GetIssuerDIDResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// DefaultManager is a package-level default manager instance.
var DefaultManager *Manager
// init initializes the default manager.
func init() {
DefaultManager = NewManager(DefaultLoaderConfig())
}
// LoadPluginWithDefaultManager is a convenience function that uses the default manager.
func LoadPluginWithDefaultManager(ctx context.Context, config *EnclaveConfig) (Plugin, error) {
return DefaultManager.LoadPlugin(ctx, config)
}
// CreateEnclaveConfig is a helper function to create enclave configuration from MPC data.
func CreateEnclaveConfig(chainID string, enclaveData *mpc.EnclaveData) *EnclaveConfig {
config := DefaultEnclaveConfig()
config.ChainID = chainID
config.EnclaveData = enclaveData
return config
}