mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
* 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>
500 lines
13 KiB
Go
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
|
|
}
|