From 8e0a6546a4abb123c0151f50fcc5e0331ad0a282 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 30 Jan 2026 08:58:16 +0530 Subject: [PATCH 1/8] feat: split event processor --- services/bridge/internal/bridge/bridge.go | 129 +++++++++++------- .../bridge/internal/bridge/event_source.go | 86 ++++++++++++ .../internal/bridge/transaction_handler.go | 82 ++++++++++- .../bridge/internal/leader/onchain_monitor.go | 6 +- .../bridge/internal/worker/worker_pool.go | 51 +++++++ 5 files changed, 298 insertions(+), 56 deletions(-) create mode 100644 services/bridge/internal/bridge/event_source.go diff --git a/services/bridge/internal/bridge/bridge.go b/services/bridge/internal/bridge/bridge.go index 50958d2..96d320a 100644 --- a/services/bridge/internal/bridge/bridge.go +++ b/services/bridge/internal/bridge/bridge.go @@ -33,7 +33,6 @@ type Bridge struct { // Channels for communication updateChan chan *bridgetypes.UpdateRequest - eventChan chan *bridgetypes.EventData errorChan chan error shutdownChan chan struct{} @@ -55,11 +54,8 @@ type Bridge struct { // On-chain monitor for replica failover onChainMonitor *leader.OnChainMonitor - // Block scanner - blockScanner BlockScanner - - // Event processor - eventProcessor *processor.GenericEventProcessor + // Event source + eventSource *EventSource // Metrics tracking metricsManager *MetricsManager @@ -120,9 +116,35 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic } destClients := make(map[int64]*WriteClient) - for _, chainConfig := range cfgService.GetEnabledChains() { + enabledChains := cfgService.GetEnabledChains() + + if len(enabledChains) == 0 { + return nil, fmt.Errorf("no enabled chains found in configuration - check that chains have 'enabled: true'") + } + + logger.Infof("Found %d enabled chain(s), attempting to create destination clients...", len(enabledChains)) + + var failedChains []string + for _, chainConfig := range enabledChains { contracts := cfgService.GetContractsForChain(chainConfig.ChainID) if len(contracts) == 0 { + logger.Warnf("Chain %d (%s): no enabled contracts found - skipping", chainConfig.ChainID, chainConfig.Name) + failedChains = append(failedChains, fmt.Sprintf("%d (%s): no enabled contracts", chainConfig.ChainID, chainConfig.Name)) + continue + } + + // Check if there's at least one enabled + hasReceiverContract := false + for _, contract := range contracts { + if (contract.Type == "receiver" || contract.Type == "pushoracle") && contract.Enabled { + hasReceiverContract = true + break + } + } + if !hasReceiverContract { + logger.Warnf("Chain %d (%s): no enabled receiver/pushoracle contracts found (found %d contracts but none are receiver/pushoracle type)", + chainConfig.ChainID, chainConfig.Name, len(contracts)) + failedChains = append(failedChains, fmt.Sprintf("%d (%s): no enabled receiver/pushoracle contract", chainConfig.ChainID, chainConfig.Name)) continue } @@ -134,14 +156,24 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic destClient, err := NewWriteClient(chainConfig, contracts, cfgService.GetInfrastructure().PrivateKey, queueManager, maxSafeGap) if err != nil { - logger.Errorf("Failed to create destination client for chain %d: %v", chainConfig.ChainID, err) + logger.Errorf("Failed to create destination client for chain %d (%s): %v", chainConfig.ChainID, chainConfig.Name, err) + failedChains = append(failedChains, fmt.Sprintf("%d (%s): %v", chainConfig.ChainID, chainConfig.Name, err)) continue } destClients[chainConfig.ChainID] = destClient + logger.Infof("Successfully created destination client for chain %d (%s)", chainConfig.ChainID, chainConfig.Name) } if len(destClients) == 0 { - return nil, fmt.Errorf("no destination clients available") + errorMsg := "no destination clients available. Reasons:\n" + if len(failedChains) > 0 { + for _, reason := range failedChains { + errorMsg += fmt.Sprintf(" - Chain %s\n", reason) + } + } else { + errorMsg += " - No enabled chains found in configuration\n" + } + return nil, fmt.Errorf(errorMsg) } workerPool := worker.NewWorkerPool( @@ -171,7 +203,6 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic readClient: readClient, writeClients: destClients, updateChan: make(chan *bridgetypes.UpdateRequest, 1000), - eventChan: eventChan, errorChan: errorChan, shutdownChan: make(chan struct{}), stats: &bridgetypes.BridgeStats{ @@ -185,13 +216,16 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic queueManager: queueManager, } + var scanner BlockScanner + var eventProcessor *processor.GenericEventProcessor + // Create block scanner if enabled if cfgService.GetInfrastructure().BlockScanner.Enabled { - scanner, err := CreateBlockScanner(cfgService, readClient, db, eventChan, errorChan) + s, err := CreateBlockScanner(cfgService, readClient, db, eventChan, errorChan) if err != nil { return nil, fmt.Errorf("failed to create block scanner: %w", err) } - bridge.blockScanner = scanner + scanner = s } // Create generic event processor @@ -202,7 +236,7 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic } } - eventProcessor, err := processor.NewGenericEventProcessor( + ep, err := processor.NewGenericEventProcessor( &cfgService.GetInfrastructure().EventProcessor, cfgService.GetEventDefinitions(), cfgService, @@ -219,7 +253,10 @@ func NewBridge(modularCfg *config.ModularConfig, cfgService *config.ConfigServic if err != nil { return nil, fmt.Errorf("failed to create event processor: %w", err) } - bridge.eventProcessor = eventProcessor + eventProcessor = ep + + // Wrap scanner and processor in EventSource + bridge.eventSource = NewEventSource(scanner, eventProcessor, eventChan, bridge.updateChan, errorChan) // Initialize chain stats bridge.initializeChainStats() @@ -340,29 +377,19 @@ func (b *Bridge) Start(ctx context.Context) error { // Start worker pool b.workerPool.Start(ctx) - // Start block scanner if enabled - if b.blockScanner != nil { - if err := b.blockScanner.Start(ctx); err != nil { - return fmt.Errorf("failed to start block scanner: %w", err) + // Start event source (scanner + processor) + if b.eventSource != nil { + if err := b.eventSource.Start(ctx); err != nil { + return fmt.Errorf("failed to start event source: %w", err) } - logger.Info("Block scanner started") - - // Start generic event processor - if b.eventProcessor != nil { - if err := b.eventProcessor.Start(ctx); err != nil { - return fmt.Errorf("failed to start event processor: %w", err) - } - logger.Info("Generic event processor started") - } - - // Start error handler - b.wg.Add(1) - go func() { - defer b.wg.Done() - b.handleErrors(ctx) - }() } + b.wg.Add(1) + go func() { + defer b.wg.Done() + b.handleErrors(ctx) + }() + // Start update processor b.wg.Add(1) go func() { @@ -432,10 +459,10 @@ func (b *Bridge) Stop(ctx context.Context) error { b.onChainMonitor.Stop() } - // Stop block scanner if running - if b.blockScanner != nil { - if err := b.blockScanner.Stop(); err != nil { - logger.Errorf("Failed to stop block scanner: %v", err) + // Stop event source (scanner + processor) + if b.eventSource != nil { + if err := b.eventSource.Stop(ctx); err != nil { + logger.Errorf("Error stopping event source: %v", err) } } @@ -471,7 +498,7 @@ func (b *Bridge) reportUpdateChanMetrics(ctx context.Context) { // Report initial size immediately (even if 0, to ensure metric is exposed) if b.metricsManager != nil { - size := len(b.updateChan) + size := b.eventSource.GetQueueSize() b.metricsManager.ReportUpdateQueueSize(size) logger.Debugf("Initial update channel size: %d", size) } else { @@ -489,7 +516,7 @@ func (b *Bridge) reportUpdateChanMetrics(ctx context.Context) { case <-ticker.C: // Periodically report updateChan size (more frequently to catch items) if b.metricsManager != nil { - size := len(b.updateChan) + size := b.eventSource.GetQueueSize() b.metricsManager.ReportUpdateQueueSize(size) // Log when queue has items if size > 0 { @@ -510,12 +537,12 @@ func (b *Bridge) processUpdates(ctx context.Context) { return case <-b.shutdownChan: return - case updateReq := <-b.updateChan: + case updateReq := <-b.eventSource.GetUpdateChan(): // Report metric: when we successfully dequeue, we know there was at least 1 item // Report current size + 1 to show the size BEFORE we dequeued this item // This gives a more accurate picture of queue depth if b.metricsManager != nil { - queueSize := len(b.updateChan) + queueSize := b.eventSource.GetQueueSize() b.metricsManager.ReportUpdateQueueSize(queueSize) } @@ -564,11 +591,15 @@ func (b *Bridge) processUpdates(ctx context.Context) { continue } - updateReq.TriggeredByMonitoring = true - logger.Infof("Processing update: monitoring check passed for chain=%d contract=%s symbol=%s", - updateReq.DestinationChain.ChainID, - updateReq.Contract.Address, - symbol) + // Only set TriggeredByMonitoring if replica failover is actually enabled + // (not just monitoring-only mode) + if b.onChainMonitor.IsEnabled() { + updateReq.TriggeredByMonitoring = true + logger.Infof("Processing update: monitoring check passed for chain=%d contract=%s symbol=%s", + updateReq.DestinationChain.ChainID, + updateReq.Contract.Address, + symbol) + } } // Create task ID based on available data @@ -600,8 +631,6 @@ func (b *Bridge) handleUpdateRequest(ctx context.Context, task *worker.WorkerTas } }() - handler := NewTransactionHandler(b.writeClients, b.routerRegistry, b.metricsManager.GetTracker()) + handler := NewTransactionHandler(b.writeClients, b.routerRegistry, b.metricsManager.GetTracker(), b.onChainMonitor) return handler.Process(ctx, task.Request) } - -// callRouterMethod calls a contract method using router configuration diff --git a/services/bridge/internal/bridge/event_source.go b/services/bridge/internal/bridge/event_source.go new file mode 100644 index 0000000..03a04b5 --- /dev/null +++ b/services/bridge/internal/bridge/event_source.go @@ -0,0 +1,86 @@ +package bridge + +import ( + "context" + "fmt" + + "github.com/diadata.org/Spectra-interoperability/pkg/logger" + "github.com/diadata.org/Spectra-interoperability/services/bridge/internal/processor" + bridgetypes "github.com/diadata.org/Spectra-interoperability/services/bridge/internal/types" +) + +// manages the scanner -> processor -> updates pipeline. +type EventSource struct { + scanner BlockScanner + processor *processor.GenericEventProcessor + eventChan chan *bridgetypes.EventData + updateChan chan *bridgetypes.UpdateRequest + errorChan chan error +} + +// NewEventSource creates a new event source +func NewEventSource( + scanner BlockScanner, + processor *processor.GenericEventProcessor, + eventChan chan *bridgetypes.EventData, + updateChan chan *bridgetypes.UpdateRequest, + errorChan chan error, +) *EventSource { + return &EventSource{ + scanner: scanner, + processor: processor, + eventChan: eventChan, + updateChan: updateChan, + errorChan: errorChan, + } +} + +// Start starts the event source components +func (s *EventSource) Start(ctx context.Context) error { + // Start scanner if available + if s.scanner != nil { + if err := s.scanner.Start(ctx); err != nil { + return fmt.Errorf("failed to start scanner: %w", err) + } + logger.Info("Event scanner started") + } + + if s.processor != nil { + if err := s.processor.Start(ctx); err != nil { + if s.scanner != nil { + s.scanner.Stop() + } + return fmt.Errorf("failed to start processor: %w", err) + } + logger.Info("Event processor started") + } + + logger.Info("EventSource started successfully") + return nil +} + +// Stop stops the event source components +func (s *EventSource) Stop(ctx context.Context) error { + logger.Info("Stopping EventSource...") + + if s.scanner != nil { + if err := s.scanner.Stop(); err != nil { + logger.Errorf("Error stopping scanner: %v", err) + return err + } + logger.Info("Event scanner stopped") + } + + logger.Info("EventSource stopped successfully") + return nil +} + +// GetUpdateChan returns the update channel for consuming processed events +func (s *EventSource) GetUpdateChan() <-chan *bridgetypes.UpdateRequest { + return s.updateChan +} + +// GetQueueSize returns the current size of the update queue +func (s *EventSource) GetQueueSize() int { + return len(s.updateChan) +} diff --git a/services/bridge/internal/bridge/transaction_handler.go b/services/bridge/internal/bridge/transaction_handler.go index 9e8a9c6..f00c4a7 100644 --- a/services/bridge/internal/bridge/transaction_handler.go +++ b/services/bridge/internal/bridge/transaction_handler.go @@ -12,6 +12,7 @@ import ( "github.com/diadata.org/Spectra-interoperability/pkg/logger" "github.com/diadata.org/Spectra-interoperability/pkg/rpc" "github.com/diadata.org/Spectra-interoperability/services/bridge/config" + "github.com/diadata.org/Spectra-interoperability/services/bridge/internal/leader" bridgetypes "github.com/diadata.org/Spectra-interoperability/services/bridge/internal/types" "github.com/diadata.org/Spectra-interoperability/services/bridge/pkg/router" ) @@ -36,14 +37,16 @@ type TransactionHandler struct { writeClients map[int64]*WriteClient routerRegistry *router.GenericRegistry metricsTracker *MetricsTracker + onChainMonitor *leader.OnChainMonitor // Optional: for replica monitoring info } // NewTransactionHandler creates a new transaction handler -func NewTransactionHandler(writeClients map[int64]*WriteClient, registry *router.GenericRegistry, tracker *MetricsTracker) *TransactionHandler { +func NewTransactionHandler(writeClients map[int64]*WriteClient, registry *router.GenericRegistry, tracker *MetricsTracker, monitor *leader.OnChainMonitor) *TransactionHandler { return &TransactionHandler{ writeClients: writeClients, routerRegistry: registry, metricsTracker: tracker, + onChainMonitor: monitor, } } @@ -67,8 +70,9 @@ func (h *TransactionHandler) Process(ctx context.Context, updateReq *bridgetypes } triggeredByMonitoring := "" - if txCtx.UpdateRequest.TriggeredByMonitoring { - triggeredByMonitoring = " (triggered by replica monitoring/failover)" + if txCtx.UpdateRequest.TriggeredByMonitoring && h.onChainMonitor != nil { + monitoringInfo := h.getMonitoringInfo(txCtx) + triggeredByMonitoring = monitoringInfo } logger.Infof("Transaction sent: %s for %s on chain %d, router=%s, symbol=%s%s", tx.Hash().Hex(), txCtx.Identifier, txCtx.UpdateRequest.DestinationChain.ChainID, @@ -315,6 +319,78 @@ func stringContains(s, substr string) bool { return false } +// getMonitoringInfo returns detailed monitoring information when transaction is triggered by replica monitoring +func (h *TransactionHandler) getMonitoringInfo(txCtx *TransactionContext) string { + if h.onChainMonitor == nil { + return " (triggered by replica monitoring/failover)" + } + + chainID := txCtx.UpdateRequest.DestinationChain.ChainID + contractAddress := common.HexToAddress(txCtx.UpdateRequest.Contract.Address) + symbol := txCtx.Symbol + + // Get configured threshold (base + 10% of base) + threshold := h.onChainMonitor.GetPriceDeviationThresholdWithOffset(chainID, contractAddress, symbol) + if threshold == nil { + return " (triggered by replica monitoring/failover)" + } + thresholdPercent := new(big.Float).Mul(threshold, big.NewFloat(100)) + + // Get on-chain value and calculate deviation + var deviationPercent *big.Float + var triggerReason string + + // Get monitoring info from monitor + onChainValue, lastTimestamp, timeThreshold := h.onChainMonitor.GetMonitoringInfo(chainID, contractAddress, symbol) + + // Calculate deviation from incoming price + if txCtx.UpdateRequest.Intent != nil && txCtx.UpdateRequest.Intent.Price != nil && onChainValue != nil && onChainValue.Sign() != 0 { + incomingPrice := txCtx.UpdateRequest.Intent.Price + diff := new(big.Int).Sub(incomingPrice, onChainValue) + oldFloat := new(big.Float).SetInt(onChainValue) + diffFloat := new(big.Float).SetInt(diff) + deviationPercent = new(big.Float).Quo(diffFloat, oldFloat) + deviationPercent.Mul(deviationPercent, big.NewFloat(100)) + } + + // Determine trigger reason + if lastTimestamp > 0 { + timeSinceUpdate := time.Since(time.Unix(int64(lastTimestamp), 0)) + totalThreshold := timeThreshold + h.onChainMonitor.GetTimeThresholdOffset() + if timeSinceUpdate > totalThreshold { + triggerReason = fmt.Sprintf("time threshold exceeded (%v > %v)", timeSinceUpdate, totalThreshold) + } else if deviationPercent != nil { + absDeviation := new(big.Float).Abs(deviationPercent) + if absDeviation.Cmp(thresholdPercent) > 0 { + triggerReason = fmt.Sprintf("price deviation threshold exceeded (%.2f%% > %.2f%%)", absDeviation, thresholdPercent) + } else { + // This shouldn't happen if we're processing, but log it for debugging + triggerReason = fmt.Sprintf("price deviation + 10%% of base not reached (%.2f%% <= %.2f%%)", absDeviation, thresholdPercent) + } + } + } + + // Build info string + info := " (triggered by replica monitoring/failover" + if thresholdPercent != nil { + info += fmt.Sprintf(", configured_threshold=%.2f%%", thresholdPercent) + } + if deviationPercent != nil { + absDeviation := new(big.Float).Abs(deviationPercent) + info += fmt.Sprintf(", deviation_from_onchain=%.2f%%", absDeviation) + } else { + info += ", deviation_from_onchain=N/A" + } + if triggerReason != "" { + info += fmt.Sprintf(", trigger=%s", triggerReason) + } else { + info += ", trigger=unknown" + } + info += ")" + + return info +} + // waitForReceipt waits for a transaction receipt func (h *TransactionHandler) waitForReceipt(ctx context.Context, client rpc.EthClient, txHash common.Hash) (*types.Receipt, error) { logger.Infof("Waiting for transaction receipt: %s", txHash.Hex()) diff --git a/services/bridge/internal/leader/onchain_monitor.go b/services/bridge/internal/leader/onchain_monitor.go index 685acdf..761c4a2 100644 --- a/services/bridge/internal/leader/onchain_monitor.go +++ b/services/bridge/internal/leader/onchain_monitor.go @@ -41,7 +41,6 @@ func (m *OnChainMonitor) IsEnabled() bool { return m.enabled } -// RouterMonitor tracks one router and its destinations type RouterMonitor struct { RouterID string destinations map[string]*DestinationMonitor @@ -74,7 +73,7 @@ func DefaultMonitorConfig() MonitorConfig { return MonitorConfig{ Enabled: false, TimeThresholdOffset: 1 * time.Minute, - PriceDeviationOffset: big.NewFloat(0.50), // 10% + PriceDeviationOffset: big.NewFloat(0.50), // 50% CheckInterval: 10 * time.Second, } } @@ -117,11 +116,12 @@ func NewOnChainMonitor( continue } + contractAddress := common.HexToAddress(dest.Contract) for _, symbol := range symbols { key := utils.GenerateDestinationKey(dest.ChainID, dest.Contract, symbol) routerMonitor.destinations[key] = &DestinationMonitor{ RouterDestination: dest, - ContractAddress: common.HexToAddress(dest.Contract), + ContractAddress: contractAddress, Client: ethClient, } } diff --git a/services/bridge/internal/worker/worker_pool.go b/services/bridge/internal/worker/worker_pool.go index 32d89c7..f426d32 100644 --- a/services/bridge/internal/worker/worker_pool.go +++ b/services/bridge/internal/worker/worker_pool.go @@ -3,6 +3,7 @@ package worker import ( "context" "sync" + "sync/atomic" "time" "github.com/diadata.org/Spectra-interoperability/pkg/logger" @@ -27,6 +28,7 @@ type WorkerPool struct { mu sync.RWMutex running bool metricsCollector *metrics.Collector + activeWorkers int32 // Track number of currently active workers } // Worker represents a single worker in the pool @@ -36,6 +38,7 @@ type Worker struct { quit chan struct{} wg *sync.WaitGroup metricsCollector *metrics.Collector + pool *WorkerPool // Reference to parent pool for metrics } // NewWorkerPool creates a new worker pool @@ -84,6 +87,7 @@ func (wp *WorkerPool) Start(ctx context.Context) { quit: make(chan struct{}), wg: &wp.wg, metricsCollector: wp.metricsCollector, + pool: wp, } wp.workers[i] = worker @@ -91,6 +95,9 @@ func (wp *WorkerPool) Start(ctx context.Context) { go worker.start(ctx) } + // Start health monitor goroutine + go wp.healthMonitor(ctx) + logger.Infof("Started worker pool with %d workers", wp.maxWorkers) } @@ -130,6 +137,46 @@ func (wp *WorkerPool) Stop(ctx context.Context) { wp.running = false } +// healthMonitor periodically monitors worker pool health and reports metrics +func (wp *WorkerPool) healthMonitor(ctx context.Context) { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-wp.shutdownChan: + return + case <-ticker.C: + activeCount := atomic.LoadInt32(&wp.activeWorkers) + queueSize := len(wp.taskQueue) + queueCap := cap(wp.taskQueue) + + // Report metrics if collector is available + if wp.metricsCollector != nil { + wp.metricsCollector.SetActiveWorkers(activeCount) + wp.metricsCollector.SetTaskQueueSize(int32(queueSize)) + } + + // Log warning if queue is getting full (>80% capacity) + if queueCap > 0 && float64(queueSize)/float64(queueCap) > 0.8 { + logger.Warnf("Worker pool queue nearing capacity: %d/%d (%.1f%%), active workers: %d/%d", + queueSize, queueCap, float64(queueSize)/float64(queueCap)*100, activeCount, wp.maxWorkers) + } + + // Log warning if all workers are busy and queue has items + if int(activeCount) >= wp.maxWorkers && queueSize > 0 { + logger.Warnf("All %d workers busy with %d tasks queued - consider increasing worker count", + wp.maxWorkers, queueSize) + } + + logger.Debugf("Worker pool health: active=%d/%d, queue=%d/%d", + activeCount, wp.maxWorkers, queueSize, queueCap) + } + } +} + // Submit submits a task to the worker pool func (wp *WorkerPool) Submit(task *WorkerTask) { if task == nil { @@ -191,6 +238,10 @@ func (w *Worker) start(ctx context.Context) { // processTask processes a single task func (w *Worker) processTask(ctx context.Context, task *WorkerTask) { + // Track active workers + atomic.AddInt32(&w.pool.activeWorkers, 1) + defer atomic.AddInt32(&w.pool.activeWorkers, -1) + startTime := time.Now() logger.Debugf("Worker %d processing task %s", w.id, task.ID) From 2ec387d8a4f30602c27d34852e95eb606e58bcac Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 30 Jan 2026 08:59:27 +0530 Subject: [PATCH 2/8] feat: attestor client type --- services/attestor/main.go | 52 ++-- .../attestor/pkg/client/diaOracleV2Client.go | 222 ++++++++++++++++++ .../attestor/pkg/client/guardianClient.go | 29 ++- services/attestor/pkg/config/config.go | 23 +- services/attestor/pkg/config/types.go | 29 ++- services/attestor/pkg/service/attestor.go | 61 ++++- 6 files changed, 384 insertions(+), 32 deletions(-) create mode 100644 services/attestor/pkg/client/diaOracleV2Client.go diff --git a/services/attestor/main.go b/services/attestor/main.go index cf204f2..c5f3cff 100644 --- a/services/attestor/main.go +++ b/services/attestor/main.go @@ -95,12 +95,13 @@ func main() { signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) logFields := map[string]interface{}{ - "symbols": cfg.Attestor.Symbols, - "oracle": cfg.Oracle.Address, - "registry": cfg.Registry.Address, - "polling_time": cfg.Attestor.PollingTime.String(), - "batch_mode": cfg.Attestor.BatchMode, - "mode": cfg.Attestor.Mode.String(), + "symbols": cfg.Attestor.Symbols, + "oracle": cfg.Oracle.Address, + "oracle_client_type": cfg.Oracle.ClientType.String(), + "registry": cfg.Registry.Address, + "polling_time": cfg.Attestor.PollingTime.String(), + "batch_mode": cfg.Attestor.BatchMode, + "mode": cfg.Attestor.Mode.String(), } if cfg.Attestor.Mode == config.ModeReplica { @@ -177,15 +178,36 @@ type dependencies struct { // createDependencies creates all the service dependencies func createDependencies(cfg *config.Config) (*dependencies, error) { - // Create oracle client - oracleClient, err := client.NewGuardedOracleClient( - cfg.RPC.URLs, - cfg.Oracle.Address, - "", // signed address not used in new architecture - cfg.Attestor.PrivateKey, - ) - if err != nil { - return nil, fmt.Errorf("failed to create oracle client: %w", err) + var oracleClient interfaces.OracleReader + var err error + + switch cfg.Oracle.ClientType { + case config.OracleClientTypeGuarded: + oracleClient, err = client.NewGuardedOracleClient( + cfg.RPC.URLs, + cfg.Oracle.Address, + "", + cfg.Attestor.PrivateKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to create guarded oracle client: %w", err) + } + logger.WithField("client_type", "guarded").Info("Using GuardedOracleClient") + + case config.OracleClientTypeDIAV2: + oracleClient, err = client.NewDIAOracleV2Client( + cfg.RPC.URLs, + cfg.Oracle.Address, + "", + cfg.Attestor.PrivateKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to create DIAOracleV2 client: %w", err) + } + logger.WithField("client_type", "dia_v2").Info("Using DIAOracleV2Client") + + default: + return nil, fmt.Errorf("unsupported oracle client type: %s", cfg.Oracle.ClientType) } // Create registry client diff --git a/services/attestor/pkg/client/diaOracleV2Client.go b/services/attestor/pkg/client/diaOracleV2Client.go new file mode 100644 index 0000000..82a10e1 --- /dev/null +++ b/services/attestor/pkg/client/diaOracleV2Client.go @@ -0,0 +1,222 @@ +package client + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + + "github.com/diadata.org/Spectra-interoperability/pkg/logger" + multirpc "github.com/diadata.org/Spectra-interoperability/pkg/rpc" + "github.com/diadata.org/Spectra-interoperability/services/attestor/pkg/config" + "github.com/diadata.org/Spectra-interoperability/services/attestor/pkg/errors" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// DIAOracleV2 ABI JSON - based on DIAOracleV2.sol contract +const diaOracleV2ABIJSON = `[ + { + "inputs": [ + { + "internalType": "string", + "name": "key", + "type": "string" + } + ], + "name": "getValue", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + } +]` + +// DIAOracleV2Client wraps access to the DIAOracleV2 on-chain oracle with RPC failover. +type DIAOracleV2Client struct { + primaryRPC string + multiClient *multirpc.MultiClient + oracleAddr common.Address + signedAddr string + privateKey string + fromAddress common.Address + oracleABI abi.ABI +} + +// NewDIAOracleV2Client creates a new DIAOracleV2 client backed by the multi-RPC failover helper. +func NewDIAOracleV2Client(rpcURLs []string, oracleAddrStr, signedAddrStr, privateKeyStr string) (*DIAOracleV2Client, error) { + if len(rpcURLs) == 0 { + return nil, fmt.Errorf("no RPC URLs provided for oracle client") + } + + multi, err := multirpc.NewMultiClient(rpcURLs) + if err != nil { + return nil, fmt.Errorf("failed to connect to DIAOracleV2 RPC endpoints: %w", err) + } + + oracleAddr := common.HexToAddress(oracleAddrStr) + oracleABI, err := abi.JSON(strings.NewReader(diaOracleV2ABIJSON)) + if err != nil { + multi.Close() + return nil, fmt.Errorf("failed to parse DIAOracleV2 ABI: %w", err) + } + + var fromAddress common.Address + if privateKeyStr != "" { + cleanPrivKey := strings.TrimPrefix(privateKeyStr, "0x") + privateKey, err := crypto.HexToECDSA(cleanPrivKey) + if err != nil { + multi.Close() + return nil, fmt.Errorf("failed to parse private key: %v", err) + } + + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + multi.Close() + return nil, fmt.Errorf("failed to cast public key to ECDSA") + } + + fromAddress = crypto.PubkeyToAddress(*publicKeyECDSA) + logger.WithField("address", fromAddress.Hex()).Debug("Derived DIAOracleV2 client address from private key") + } else { + fromAddress = common.Address{} + } + + return &DIAOracleV2Client{ + primaryRPC: rpcURLs[0], + multiClient: multi, + oracleAddr: oracleAddr, + signedAddr: signedAddrStr, + privateKey: privateKeyStr, + fromAddress: fromAddress, + oracleABI: oracleABI, + }, nil +} + +// Close releases the underlying RPC connections. +func (oc *DIAOracleV2Client) Close() { + if oc.multiClient != nil { + oc.multiClient.Close() + } +} + +// GetValue fetches the latest oracle value from DIAOracleV2 contract. +func (oc *DIAOracleV2Client) GetValue(ctx context.Context, symbol string) (*big.Int, *big.Int, error) { + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getValue", + }).Debug("Calling DIAOracleV2 contract function: getValue") + + price, timestamp, err := oc.fetchOracleValue(ctx, symbol) + if err != nil { + return nil, nil, errors.NewOracleError(symbol, "failed to get value (getValue)", err) + } + + if price == nil || price.Sign() <= 0 { + return nil, nil, errors.NewOracleError(symbol, "invalid price", nil) + } + + if timestamp == nil || timestamp.Sign() <= 0 { + return nil, nil, errors.NewOracleError(symbol, "invalid timestamp", nil) + } + + return price, timestamp, nil +} + +// GetGuardedValue fetches the latest oracle value from DIAOracleV2 contract. +// Note: DIAOracleV2 doesn't support guardian validation, so this method calls GetValue internally +// and ignores the guardian parameters. +func (oc *DIAOracleV2Client) GetGuardedValue(ctx context.Context, symbol string, params config.GuardianParams) (*big.Int, *big.Int, error) { + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "client_type": "dia_v2", + "contract_function": "getValue(string)", + "MaxDeviationBips": params.MaxDeviationBips, + "MaxTimestampAge": params.MaxTimestampAge, + "MinGuardianMatches": params.MinGuardianMatches, + }).Info("DIAOracleV2Client: Guardian parameters ignored (not supported), calling getValue(string) instead of getGuardedValue") + return oc.GetValue(ctx, symbol) +} + +func (oc *DIAOracleV2Client) fetchOracleValue(ctx context.Context, symbol string) (*big.Int, *big.Int, error) { + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getValue(string)", + }).Debug("Packing contract call for DIAOracleV2.getValue") + + data, err := oc.oracleABI.Pack("getValue", symbol) + if err != nil { + return nil, nil, fmt.Errorf("failed to pack input data for getValue: %v", err) + } + + callMsg := ethereum.CallMsg{To: &oc.oracleAddr, Data: data} + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getValue(string)", + }).Info("Calling DIAOracleV2 contract: getValue") + + resultBytes, err := oc.multiClient.CallContract(ctx, callMsg, nil) + if err != nil { + return nil, nil, fmt.Errorf("contract call failed for getValue(%s): %v", symbol, err) + } + + outputs, err := oc.oracleABI.Unpack("getValue", resultBytes) + if err != nil { + return nil, nil, fmt.Errorf("failed to unpack result: %v", err) + } + + if len(outputs) != 2 { + return nil, nil, fmt.Errorf("unexpected number of outputs: got %d, want 2", len(outputs)) + } + + price, ok := outputs[0].(*big.Int) + if !ok { + return nil, nil, fmt.Errorf("failed to convert price to big.Int") + } + + timestamp, ok := outputs[1].(*big.Int) + if !ok { + return nil, nil, fmt.Errorf("failed to convert timestamp to big.Int") + } + + return price, timestamp, nil +} + +// Accessors retained for compatibility. +func (oc *DIAOracleV2Client) GetRPCURL() string { + return oc.primaryRPC +} + +func (oc *DIAOracleV2Client) GetOracleAddr() string { + return oc.oracleAddr.Hex() +} + +func (oc *DIAOracleV2Client) GetSignedAddr() string { + return oc.signedAddr +} + +func (oc *DIAOracleV2Client) GetPrivateKey() string { + return oc.privateKey +} + +func (oc *DIAOracleV2Client) GetFromAddress() string { + return oc.fromAddress.Hex() +} diff --git a/services/attestor/pkg/client/guardianClient.go b/services/attestor/pkg/client/guardianClient.go index 15769fb..6d9f8cc 100644 --- a/services/attestor/pkg/client/guardianClient.go +++ b/services/attestor/pkg/client/guardianClient.go @@ -373,9 +373,19 @@ func (oc *GuardedOracleClient) GetGuardedValue(ctx context.Context, symbol strin maxTimestampAge := big.NewInt(int64(params.MaxTimestampAge)) numMinGuardianMatches := big.NewInt(int64(params.MinGuardianMatches)) + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "client_type": "guarded", + "contract_function": "getGuardedValue(string,uint256,uint256,uint256)", + "maxDeviationBips": params.MaxDeviationBips, + "maxTimestampAge": params.MaxTimestampAge, + "minGuardianMatches": params.MinGuardianMatches, + }).Info("GuardedOracleClient: Calling getGuardedValue with guardian validation") + price, timestamp, err := oc.fetchOracleValue(ctx, symbol, maxDeviationBips, maxTimestampAge, numMinGuardianMatches) if err != nil { - return nil, nil, errors.NewOracleError(symbol, "failed to get value", err) + return nil, nil, errors.NewOracleError(symbol, "failed to get value (getGuardedValue)", err) } if price == nil || price.Sign() <= 0 { @@ -399,15 +409,28 @@ func (oc *GuardedOracleClient) GetValue(ctx context.Context, symbol string) (*bi } func (oc *GuardedOracleClient) fetchOracleValue(ctx context.Context, symbol string, maxDeviationBips, maxTimestampAge, numMinGuardianMatches *big.Int) (*big.Int, *big.Int, error) { + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getGuardedValue(string,uint256,uint256,uint256)", + }).Debug("Packing contract call for GuardedOracle.getGuardedValue") + data, err := oc.oracleABI.Pack("getGuardedValue", symbol, maxDeviationBips, maxTimestampAge, numMinGuardianMatches) if err != nil { - return nil, nil, fmt.Errorf("failed to pack input data: %v", err) + return nil, nil, fmt.Errorf("failed to pack input data for getGuardedValue: %v", err) } callMsg := ethereum.CallMsg{To: &oc.oracleAddr, Data: data} + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getGuardedValue(string,uint256,uint256,uint256)", + }).Info("Calling GuardedOracle contract: getGuardedValue") + resultBytes, err := oc.multiClient.CallContract(ctx, callMsg, nil) if err != nil { - return nil, nil, fmt.Errorf("contract call failed: %v", err) + return nil, nil, fmt.Errorf("contract call failed for getGuardedValue(%s, %d, %d, %d): %v", + symbol, maxDeviationBips.Int64(), maxTimestampAge.Int64(), numMinGuardianMatches.Int64(), err) } outputs, err := oc.oracleABI.Unpack("getGuardedValue", resultBytes) diff --git a/services/attestor/pkg/config/config.go b/services/attestor/pkg/config/config.go index 6eff807..7b22519 100644 --- a/services/attestor/pkg/config/config.go +++ b/services/attestor/pkg/config/config.go @@ -29,7 +29,8 @@ type Config struct { } `mapstructure:"rpc"` Oracle struct { - Address string `mapstructure:"address"` + Address string `mapstructure:"address"` + ClientType OracleClientType `mapstructure:"client_type"` } `mapstructure:"oracle"` Registry struct { @@ -119,11 +120,16 @@ func Init(configPath string) (*Config, error) { v.BindEnv("attestor.replica_backup_delay", "ATTESTOR_ATTESTOR_REPLICA_BACKUP_DELAY") v.BindEnv("attestor.intent_type", "ATTESTOR_ATTESTOR_INTENT_TYPE") v.BindEnv("attestor.intent_version", "ATTESTOR_ATTESTOR_INTENT_VERSION") + v.BindEnv("oracle.client_type", "ATTESTOR_ORACLE_CLIENT_TYPE") + v.BindEnv("attestor.guardian.default.max_deviation_bips", "ATTESTOR_GUARDIAN_MAX_DEVIATION_BIPS") + v.BindEnv("attestor.guardian.default.max_timestamp_age", "ATTESTOR_GUARDIAN_MAX_TIMESTAMP_AGE") + v.BindEnv("attestor.guardian.default.min_guardian_matches", "ATTESTOR_GUARDIAN_MIN_GUARDIAN_MATCHES") // Set defaults v.SetDefault("rpc.url", "https://testnet-rpc.diadata.org") v.SetDefault("rpc.registry_url", "https://testnet-rpc.diadata.org") v.SetDefault("oracle.address", "0x0087342f5f4c7AB23a37c045c3EF710749527c88") + v.SetDefault("oracle.client_type", "guarded") v.SetDefault("attestor.symbols", []string{"BTC/USD", "ETH/USD"}) v.SetDefault("attestor.polling_time", "300ms") v.SetDefault("attestor.batch_mode", true) @@ -156,6 +162,17 @@ func Init(configPath string) (*Config, error) { return nil, fmt.Errorf("invalid attestor mode: %s (must be 'prime' or 'replica')", cfg.Attestor.Mode) } + // Parse and validate oracle client type + if cfg.Oracle.ClientType == "" { + cfg.Oracle.ClientType = OracleClientTypeGuarded // Default to guarded + } else { + clientType, err := ParseOracleClientType(string(cfg.Oracle.ClientType)) + if err != nil { + return nil, fmt.Errorf("invalid oracle client type: %w", err) + } + cfg.Oracle.ClientType = clientType + } + // Normalize RPC URLs configuration: convert single URL to array if needed if len(cfg.RPC.URLs) == 0 { if cfg.RPC.URL != "" { @@ -198,6 +215,10 @@ func validateConfig(cfg *Config) error { return fmt.Errorf("oracle address not configured") } + if !cfg.Oracle.ClientType.IsValid() { + return fmt.Errorf("invalid oracle client type: %s (must be 'guarded' or 'dia_v2')", cfg.Oracle.ClientType) + } + if cfg.Registry.Address == "" { return fmt.Errorf("registry address not configured") } diff --git a/services/attestor/pkg/config/types.go b/services/attestor/pkg/config/types.go index 3810f6a..49b1aa0 100644 --- a/services/attestor/pkg/config/types.go +++ b/services/attestor/pkg/config/types.go @@ -31,6 +31,32 @@ func ParseAttestorMode(s string) (AttestorMode, error) { return mode, nil } +type OracleClientType string + +const ( + OracleClientTypeGuarded OracleClientType = "guarded" + OracleClientTypeDIAV2 OracleClientType = "dia_v2" +) + +// String returns the string representation of the oracle client type +func (t OracleClientType) String() string { + return string(t) +} + +// IsValid checks if the oracle client type is valid +func (t OracleClientType) IsValid() bool { + return t == OracleClientTypeGuarded || t == OracleClientTypeDIAV2 +} + +// ParseOracleClientType parses a string into OracleClientType +func ParseOracleClientType(s string) (OracleClientType, error) { + clientType := OracleClientType(s) + if !clientType.IsValid() { + return "", fmt.Errorf("invalid oracle client type: %s (must be 'guarded' or 'dia_v2')", s) + } + return clientType, nil +} + // AttestorConfig holds attestor-specific configuration type AttestorConfig struct { PrivateKey string `mapstructure:"private_key"` @@ -43,7 +69,8 @@ type AttestorConfig struct { // OracleConfig holds oracle configuration type OracleConfig struct { - Address string `mapstructure:"address"` + Address string `mapstructure:"address"` + ClientType OracleClientType `mapstructure:"client_type"` } // RegistryConfig holds registry configuration diff --git a/services/attestor/pkg/service/attestor.go b/services/attestor/pkg/service/attestor.go index 36a77bb..58fae49 100644 --- a/services/attestor/pkg/service/attestor.go +++ b/services/attestor/pkg/service/attestor.go @@ -211,18 +211,43 @@ func (s *AttestorService) processSingleAttestation(ctx context.Context, symbol s price, timestamp, err := s.oracle.GetGuardedValue(ctx, symbol, guardianParams) s.metrics.RecordOracleFetchDuration(symbol, time.Since(fetchStart)) - logger.WithFields(map[string]interface{}{ - "symbol": symbol, - "price": price.String(), - "timestamp": timestamp.String(), - "MaxDeviationBips": guardianParams.MaxDeviationBips, - "MaxTimestampAge": guardianParams.MaxTimestampAge, - "MinGuardianMatches": guardianParams.MinGuardianMatches, - }).Info("Retrieving oracle value") + // Log oracle value retrieval with appropriate detail level based on client type + logFields := map[string]interface{}{ + "symbol": symbol, + "price": "", + "timestamp": "", + } + if price != nil { + logFields["price"] = price.String() + } + if timestamp != nil { + logFields["timestamp"] = timestamp.String() + } + + if s.config.Oracle.ClientType == config.OracleClientTypeGuarded { + logFields["MaxDeviationBips"] = guardianParams.MaxDeviationBips + logFields["MaxTimestampAge"] = guardianParams.MaxTimestampAge + logFields["MinGuardianMatches"] = guardianParams.MinGuardianMatches + } + + logger.WithFields(logFields).Info("Retrieving oracle value") if err != nil { s.metrics.RecordIntentCreated(symbol, false) - return errors.NewOracleError(symbol, "failed to fetch value", err) + var contractFunction string + if s.config.Oracle.ClientType == config.OracleClientTypeGuarded { + contractFunction = "getGuardedValue(string,uint256,uint256,uint256)" + } else { + contractFunction = "getValue(string)" + } + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": s.config.Oracle.Address, + "client_type": s.config.Oracle.ClientType.String(), + "contract_function": contractFunction, + "error": err.Error(), + }).Error("Failed to fetch oracle value - check if symbol exists in oracle contract") + return errors.NewOracleError(symbol, fmt.Sprintf("failed to fetch value (called %s)", contractFunction), err) } // Default volume @@ -301,18 +326,30 @@ symbolLoop: s.metrics.RecordOracleFetchDuration(symbol, time.Since(fetchStart)) if err != nil { - logger.WithError(err).WithField("symbol", symbol).Error("Failed to fetch oracle value") + var contractFunction string + if s.config.Oracle.ClientType == config.OracleClientTypeGuarded { + contractFunction = "getGuardedValue(string,uint256,uint256,uint256)" + } else { + contractFunction = "getValue(string)" + } + logger.WithError(err).WithFields(map[string]interface{}{ + "symbol": symbol, + "oracle_address": s.config.Oracle.Address, + "client_type": s.config.Oracle.ClientType.String(), + "contract_function": contractFunction, + }).Error("Failed to fetch oracle value - check if symbol exists in oracle contract") s.metrics.RecordIntentCreated(symbol, false) continue } volume := big.NewInt(1) - logger.WithFields(map[string]interface{}{ + logFields := map[string]interface{}{ "symbol": symbol, "price": price.String(), "timestamp": timestamp.String(), - }).Debug("Retrieved oracle value") + } + logger.WithFields(logFields).Debug("Retrieved oracle value") symbolData = append(symbolData, interfaces.SymbolData{ Symbol: symbol, From 4bf1844bf70e8c078dd349fd26e4b6cf23e7f2d1 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 30 Jan 2026 09:22:18 +0530 Subject: [PATCH 3/8] chore: update script to monotr local setup --- scripts/monitor-receiver.sh | 592 ++++++++++++++++++++++++++++++++++ scripts/start-local.sh | 611 +++++++++++++++++++++++++++++++----- 2 files changed, 1132 insertions(+), 71 deletions(-) create mode 100755 scripts/monitor-receiver.sh diff --git a/scripts/monitor-receiver.sh b/scripts/monitor-receiver.sh new file mode 100755 index 0000000..ff93f2d --- /dev/null +++ b/scripts/monitor-receiver.sh @@ -0,0 +1,592 @@ +#!/usr/bin/env bash +set -euo pipefail + +####################################### +# PushOracleReceiverV2 Monitor +# Monitors oracle updates and logs: +# - Received updates +# - Expected updates (based on deviation) +# - Missed updates +####################################### + +# Configuration +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LOG_DIR="${ROOT_DIR}/.local-stack/logs" +LOG_FILE="${LOG_DIR}/receiver-monitor.log" +STATE_FILE="${LOG_DIR}/receiver-state.json" +METRICS_FILE="${LOG_DIR}/receiver-metrics.json" + +RPC_URL="${RPC_URL:-http://localhost:8545}" +RECEIVER_ADDR_FILE="${ROOT_DIR}/.local-stack/contracts/push_oracle_receiver_v2.addr" +DIA_ORACLE_ADDR_FILE="${ROOT_DIR}/.local-stack/contracts/dia_oracle_v2.addr" + +# Monitoring parameters +POLL_INTERVAL="${POLL_INTERVAL:-5}" # Seconds between checks +DEVIATION_THRESHOLD="${DEVIATION_THRESHOLD:-0.5}" # Percentage deviation to trigger update +TIME_THRESHOLD="${TIME_THRESHOLD:-120}" # Seconds before update is expected +SYMBOLS=("ETH/USD" "BTC/USD") + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +####################################### +# Initialization +####################################### + +init() { + mkdir -p "$LOG_DIR" + + # Check if receiver address exists + if [ ! -f "$RECEIVER_ADDR_FILE" ]; then + echo "Error: Receiver address file not found: $RECEIVER_ADDR_FILE" + echo "Run ./scripts/start-local.sh first" + exit 1 + fi + + RECEIVER_ADDR=$(cat "$RECEIVER_ADDR_FILE") + + # Check if DIA oracle address exists + if [ -f "$DIA_ORACLE_ADDR_FILE" ]; then + DIA_ORACLE_ADDR=$(cat "$DIA_ORACLE_ADDR_FILE") + else + DIA_ORACLE_ADDR="" + fi + + # Initialize state file if not exists + if [ ! -f "$STATE_FILE" ]; then + echo '{}' > "$STATE_FILE" + fi + + # Initialize metrics file + cat > "$METRICS_FILE" << EOF +{ + "started_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")", + "total_updates": 0, + "total_deviations_detected": 0, + "total_missed_updates": 0, + "symbols": {} +} +EOF + + log_info "Monitor initialized" + log_info "Receiver: $RECEIVER_ADDR" + log_info "DIA Oracle: ${DIA_ORACLE_ADDR:-N/A}" + log_info "Deviation threshold: ${DEVIATION_THRESHOLD}%" + log_info "Time threshold: ${TIME_THRESHOLD}s" + log_info "Poll interval: ${POLL_INTERVAL}s" + log_info "Log file: $LOG_FILE" +} + +####################################### +# Logging Functions +####################################### + +timestamp() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +log_to_file() { + local level="$1" + local message="$2" + echo "[$(timestamp)] [$level] $message" >> "$LOG_FILE" +} + +log_info() { + echo -e "${BLUE}[$(timestamp)]${NC} ${CYAN}[INFO]${NC} $1" + log_to_file "INFO" "$1" +} + +log_update() { + echo -e "${BLUE}[$(timestamp)]${NC} ${GREEN}[UPDATE]${NC} $1" + log_to_file "UPDATE" "$1" +} + +log_deviation() { + echo -e "${BLUE}[$(timestamp)]${NC} ${YELLOW}[DEVIATION]${NC} $1" + log_to_file "DEVIATION" "$1" +} + +log_missed() { + echo -e "${BLUE}[$(timestamp)]${NC} ${RED}[MISSED]${NC} $1" + log_to_file "MISSED" "$1" +} + +log_warning() { + echo -e "${BLUE}[$(timestamp)]${NC} ${YELLOW}[WARNING]${NC} $1" + log_to_file "WARNING" "$1" +} + +log_error() { + echo -e "${BLUE}[$(timestamp)]${NC} ${RED}[ERROR]${NC} $1" + log_to_file "ERROR" "$1" +} + +####################################### +# Contract Query Functions +####################################### + +# Get price from PushOracleReceiverV2 (returns "value timestamp" as integers) +get_receiver_price() { + local symbol="$1" + local result + result=$(cast call --rpc-url "$RPC_URL" "$RECEIVER_ADDR" \ + "getValue(string)(uint128,uint128)" "$symbol" 2>/dev/null || echo "0\n0") + # Parse and normalize the output + # Format is: "value [scientific]\ntimestamp [scientific]" + python3 -c " +import re +raw = '''$result''' +lines = raw.strip().split('\n') +val = 0 +ts = 0 +try: + if len(lines) >= 2: + # First line is value, second line is timestamp + # Each line format: 'number [scientific]' - we want the first number + val_match = re.match(r'^(\d+)', lines[0].strip()) + ts_match = re.match(r'^(\d+)', lines[1].strip()) + if val_match: + val = int(val_match.group(1)) + if ts_match: + ts = int(ts_match.group(1)) + elif len(lines) == 1: + # Single line with space-separated values + parts = lines[0].split() + if len(parts) >= 2: + val = int(re.match(r'^(\d+)', parts[0]).group(1)) + ts = int(re.match(r'^(\d+)', parts[1]).group(1)) +except Exception as e: + pass +print(f'{val} {ts}') +" 2>/dev/null || echo "0 0" +} + +# Get price from DIAOracleV2 (source oracle) +get_source_price() { + local symbol="$1" + if [ -z "$DIA_ORACLE_ADDR" ]; then + echo "0 0" + return + fi + local result + result=$(cast call --rpc-url "$RPC_URL" "$DIA_ORACLE_ADDR" \ + "getValue(string)(uint128,uint128)" "$symbol" 2>/dev/null || echo "0\n0") + # Parse and normalize the output + python3 -c " +import re +raw = '''$result''' +lines = raw.strip().split('\n') +val = 0 +ts = 0 +try: + if len(lines) >= 2: + val_match = re.match(r'^(\d+)', lines[0].strip()) + ts_match = re.match(r'^(\d+)', lines[1].strip()) + if val_match: + val = int(val_match.group(1)) + if ts_match: + ts = int(ts_match.group(1)) + elif len(lines) == 1: + parts = lines[0].split() + if len(parts) >= 2: + val = int(re.match(r'^(\d+)', parts[0]).group(1)) + ts = int(re.match(r'^(\d+)', parts[1]).group(1)) +except Exception as e: + pass +print(f'{val} {ts}') +" 2>/dev/null || echo "0 0" +} + +# Convert wei to human readable price +wei_to_price() { + local wei="$1" + if [ "$wei" = "0" ] || [ -z "$wei" ]; then + echo "0.00" + return + fi + python3 -c " +try: + val = int('$wei') + print(f'{val / 1e18:.2f}') +except: + print('0.00') +" 2>/dev/null || echo "0.00" +} + +# Calculate percentage deviation +calc_deviation() { + local old_price="$1" + local new_price="$2" + if [ "$old_price" = "0" ] || [ -z "$old_price" ]; then + echo "100.00" + return + fi + python3 -c " +try: + old = int('$old_price') + new = int('$new_price') + if old == 0: + print('100.00') + else: + deviation = abs(new - old) / old * 100 + print(f'{deviation:.4f}') +except: + print('0.00') +" 2>/dev/null || echo "0.00" +} + +####################################### +# State Management +####################################### + +# Get previous state for a symbol +get_state() { + local symbol="$1" + local key=$(echo "$symbol" | tr '/' '_') + python3 -c " +import json +try: + with open('$STATE_FILE', 'r') as f: + state = json.load(f) + data = state.get('$key', {}) + print(f\"{data.get('value', '0')} {data.get('timestamp', '0')} {data.get('source_value', '0')} {data.get('source_timestamp', '0')} {data.get('last_deviation_time', '0')}\") +except: + print('0 0 0 0 0') +" 2>/dev/null || echo "0 0 0 0 0" +} + +# Save state for a symbol +save_state() { + local symbol="$1" + local value="$2" + local timestamp="$3" + local source_value="$4" + local source_timestamp="$5" + local last_deviation_time="${6:-0}" + local key=$(echo "$symbol" | tr '/' '_') + + python3 -c " +import json +try: + with open('$STATE_FILE', 'r') as f: + state = json.load(f) +except: + state = {} + +state['$key'] = { + 'value': '$value', + 'timestamp': '$timestamp', + 'source_value': '$source_value', + 'source_timestamp': '$source_timestamp', + 'last_deviation_time': '$last_deviation_time', + 'last_check': '$(timestamp)' +} + +with open('$STATE_FILE', 'w') as f: + json.dump(state, f, indent=2) +" 2>/dev/null +} + +# Update metrics +update_metrics() { + local event_type="$1" + local symbol="$2" + local details="${3:-}" + + python3 -c " +import json +from datetime import datetime + +try: + with open('$METRICS_FILE', 'r') as f: + metrics = json.load(f) +except: + metrics = {'symbols': {}} + +# Update counters +if '$event_type' == 'update': + metrics['total_updates'] = metrics.get('total_updates', 0) + 1 +elif '$event_type' == 'deviation': + metrics['total_deviations_detected'] = metrics.get('total_deviations_detected', 0) + 1 +elif '$event_type' == 'missed': + metrics['total_missed_updates'] = metrics.get('total_missed_updates', 0) + 1 + +# Update per-symbol metrics +symbol_key = '$symbol'.replace('/', '_') +if symbol_key not in metrics['symbols']: + metrics['symbols'][symbol_key] = { + 'updates': 0, + 'deviations': 0, + 'missed': 0, + 'last_update': None, + 'last_price': None + } + +sym = metrics['symbols'][symbol_key] +if '$event_type' == 'update': + sym['updates'] += 1 + sym['last_update'] = '$(timestamp)' + if '$details': + sym['last_price'] = '$details' +elif '$event_type' == 'deviation': + sym['deviations'] += 1 +elif '$event_type' == 'missed': + sym['missed'] += 1 + +metrics['last_check'] = '$(timestamp)' + +with open('$METRICS_FILE', 'w') as f: + json.dump(metrics, f, indent=2) +" 2>/dev/null +} + +####################################### +# Monitoring Logic +####################################### + +check_symbol() { + local symbol="$1" + local now=$(date +%s) + + # Get current receiver price + local receiver_result=$(get_receiver_price "$symbol") + local receiver_value=$(echo "$receiver_result" | awk '{print $1}') + local receiver_timestamp=$(echo "$receiver_result" | awk '{print $2}') + + # Get source oracle price + local source_result=$(get_source_price "$symbol") + local source_value=$(echo "$source_result" | awk '{print $1}') + local source_timestamp=$(echo "$source_result" | awk '{print $2}') + + # Get previous state + local prev_state=$(get_state "$symbol") + local prev_value=$(echo "$prev_state" | awk '{print $1}') + local prev_timestamp=$(echo "$prev_state" | awk '{print $2}') + local prev_source_value=$(echo "$prev_state" | awk '{print $3}') + local prev_deviation_time=$(echo "$prev_state" | awk '{print $5}') + + # Convert to human readable + local receiver_price=$(wei_to_price "$receiver_value") + local source_price=$(wei_to_price "$source_value") + local prev_receiver_price=$(wei_to_price "$prev_value") + + # Check for new update (use Python for safe comparison) + local is_new_update=$(python3 -c "print('yes' if '$receiver_timestamp' != '$prev_timestamp' and '$receiver_timestamp' != '0' else 'no')" 2>/dev/null || echo "no") + if [ "$is_new_update" = "yes" ]; then + log_update "$symbol: New update received! Price: \$$receiver_price (ts: $receiver_timestamp)" + update_metrics "update" "$symbol" "$receiver_price" + fi + + # Check deviation between source and receiver + local has_values=$(python3 -c "print('yes' if '$source_value' != '0' and '$receiver_value' != '0' else 'no')" 2>/dev/null || echo "no") + if [ "$has_values" = "yes" ]; then + local deviation=$(calc_deviation "$receiver_value" "$source_value") + local deviation_exceeded=$(python3 -c "print('yes' if float('$deviation') >= float('$DEVIATION_THRESHOLD') else 'no')" 2>/dev/null || echo "no") + + if [ "$deviation_exceeded" = "yes" ]; then + log_deviation "$symbol: Deviation detected! Source: \$$source_price, Receiver: \$$receiver_price, Deviation: ${deviation}%" + update_metrics "deviation" "$symbol" + + # Check if update is overdue (use Python for arithmetic) + local time_check=$(python3 -c " +try: + now = int('$now') + ts = int('$receiver_timestamp') + threshold = int('$TIME_THRESHOLD') + time_since = now - ts + if time_since > threshold: + print(f'overdue {time_since}') + else: + print('ok') +except: + print('ok') +" 2>/dev/null || echo "ok") + + if [[ "$time_check" == overdue* ]]; then + local time_since=$(echo "$time_check" | awk '{print $2}') + log_missed "$symbol: UPDATE EXPECTED! Deviation: ${deviation}% > ${DEVIATION_THRESHOLD}%, Time since update: ${time_since}s > ${TIME_THRESHOLD}s" + update_metrics "missed" "$symbol" + fi + fi + fi + + # Check for stale data (use Python for arithmetic) + local stale_check=$(python3 -c " +try: + ts = int('$receiver_timestamp') + if ts == 0: + print('no_data') + else: + now = int('$now') + threshold = int('$TIME_THRESHOLD') * 2 + age = now - ts + if age > threshold: + print(f'stale {age}') + else: + print('ok') +except: + print('ok') +" 2>/dev/null || echo "ok") + + if [[ "$stale_check" == stale* ]]; then + local age=$(echo "$stale_check" | awk '{print $2}') + log_warning "$symbol: Data is stale! Last update: ${age}s ago" + fi + + # Save current state + save_state "$symbol" "$receiver_value" "$receiver_timestamp" "$source_value" "$source_timestamp" "$now" +} + +print_status() { + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo " PushOracleReceiverV2 Monitor Status - $(timestamp)" + echo "═══════════════════════════════════════════════════════════════" + + for symbol in "${SYMBOLS[@]}"; do + local receiver_result=$(get_receiver_price "$symbol") + local receiver_value=$(echo "$receiver_result" | awk '{print $1}') + local receiver_timestamp=$(echo "$receiver_result" | awk '{print $2}') + local receiver_price=$(wei_to_price "$receiver_value") + + local source_result=$(get_source_price "$symbol") + local source_value=$(echo "$source_result" | awk '{print $1}') + local source_price=$(wei_to_price "$source_value") + + local now=$(date +%s) + + # Calculate age using Python + local age=$(python3 -c " +try: + ts = int('$receiver_timestamp') + if ts == 0: + print('N/A (no data)') + else: + now = int('$now') + age = now - ts + print(f'{age}s ago') +except: + print('N/A') +" 2>/dev/null || echo "N/A") + + # Calculate deviation + local deviation=$(python3 -c " +try: + src = int('$source_value') + rcv = int('$receiver_value') + if src == 0 or rcv == 0: + print('N/A') + else: + dev = abs(src - rcv) / rcv * 100 + print(f'{dev:.4f}%') +except: + print('N/A') +" 2>/dev/null || echo "N/A") + + echo "" + echo " $symbol:" + echo " Receiver Price: \$$receiver_price" + echo " Source Price: \$$source_price" + echo " Deviation: $deviation" + echo " Last Update: $age" + done + + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo "" +} + +####################################### +# Main Loop +####################################### + +monitor_loop() { + log_info "Starting monitoring loop (Ctrl+C to stop)..." + + # Print initial status + print_status + + while true; do + for symbol in "${SYMBOLS[@]}"; do + check_symbol "$symbol" + done + sleep "$POLL_INTERVAL" + done +} + +show_help() { + echo "PushOracleReceiverV2 Monitor" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " start Start continuous monitoring (default)" + echo " status Show current status once" + echo " logs Show recent log entries" + echo " metrics Show metrics summary" + echo " help Show this help" + echo "" + echo "Environment Variables:" + echo " RPC_URL RPC endpoint (default: http://localhost:8545)" + echo " POLL_INTERVAL Seconds between checks (default: 5)" + echo " DEVIATION_THRESHOLD Deviation % to trigger alert (default: 0.5)" + echo " TIME_THRESHOLD Seconds before update expected (default: 120)" +} + +show_metrics() { + if [ -f "$METRICS_FILE" ]; then + echo "=== Receiver Metrics ===" + cat "$METRICS_FILE" | python3 -m json.tool 2>/dev/null || cat "$METRICS_FILE" + else + echo "No metrics file found. Run monitor first." + fi +} + +show_logs() { + if [ -f "$LOG_FILE" ]; then + echo "=== Recent Logs (last 50 lines) ===" + tail -50 "$LOG_FILE" + else + echo "No log file found. Run monitor first." + fi +} + +####################################### +# Entry Point +####################################### + +main() { + local command="${1:-start}" + + case "$command" in + start) + init + monitor_loop + ;; + status) + init + print_status + ;; + logs) + show_logs + ;; + metrics) + show_metrics + ;; + help|--help|-h) + show_help + ;; + *) + echo "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/start-local.sh b/scripts/start-local.sh index ef00c0b..002852c 100755 --- a/scripts/start-local.sh +++ b/scripts/start-local.sh @@ -10,6 +10,83 @@ NC='\033[0m' # No Color # Global variables ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +####################################### +# DEPENDENCY CHECKS +####################################### + +check_dependencies() { + log_info "Checking required dependencies..." + local missing_deps=() + + # Check Docker + if ! command -v docker &> /dev/null; then + missing_deps+=("docker") + else + if ! docker info &> /dev/null; then + log_error "Docker is installed but not running. Please start Docker Desktop." + exit 1 + fi + log_success "Docker: $(docker --version | cut -d' ' -f3 | tr -d ',')" + fi + + # Check Docker Compose + if ! docker compose version &> /dev/null; then + missing_deps+=("docker-compose") + else + log_success "Docker Compose: $(docker compose version --short)" + fi + + # Check Foundry tools (anvil, cast, forge) + if ! command -v anvil &> /dev/null; then + missing_deps+=("anvil (Foundry)") + else + log_success "Anvil: $(anvil --version 2>/dev/null | head -1 | cut -d' ' -f2)" + fi + + if ! command -v cast &> /dev/null; then + missing_deps+=("cast (Foundry)") + else + log_success "Cast: $(cast --version 2>/dev/null | head -1 | cut -d' ' -f2)" + fi + + if ! command -v forge &> /dev/null; then + missing_deps+=("forge (Foundry)") + else + log_success "Forge: $(forge --version 2>/dev/null | head -1 | cut -d' ' -f2)" + fi + + # Check Python3 + if ! command -v python3 &> /dev/null; then + missing_deps+=("python3") + else + log_success "Python3: $(python3 --version | cut -d' ' -f2)" + fi + + # Check Go (optional, only needed for local builds) + if command -v go &> /dev/null; then + log_success "Go: $(go version | cut -d' ' -f3)" + else + log_warning "Go not found (optional, Docker builds will work)" + fi + + # Report missing dependencies + if [ ${#missing_deps[@]} -gt 0 ]; then + log_error "Missing required dependencies:" + for dep in "${missing_deps[@]}"; do + echo " - $dep" + done + echo "" + log_info "Installation instructions:" + echo " Docker: https://docs.docker.com/get-docker/" + echo " Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup" + echo " Python3: brew install python3 (macOS) or apt install python3 (Linux)" + exit 1 + fi + + log_success "All dependencies satisfied!" + echo "" +} CONTRACTS_DIR="${ROOT_DIR}/contracts" LOCAL_CONTRACTS_DIR="${CONTRACTS_DIR}" ATTESTOR_DIR="${ROOT_DIR}/services/attestor" @@ -43,6 +120,8 @@ REGISTRY_ADDR_FILE="${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr" RECEIVER_ADDR_FILE="${CONTRACTS_ADDR_DIR}/push_oracle_receiver_v2.addr" PROTOCOL_FEE_HOOK_FILE="${CONTRACTS_ADDR_DIR}/protocol_fee_hook.addr" DIA_ORACLE_ADDR_FILE="${CONTRACTS_ADDR_DIR}/dia_oracle_v2.addr" +DIA_ORACLE_RANDOMNESS_ADDR_FILE="${CONTRACTS_ADDR_DIR}/dia_oracle_randomness.addr" +RANDOM_REQUEST_MANAGER_ADDR_FILE="${CONTRACTS_ADDR_DIR}/random_request_manager.addr" # Logging functions log_info() { @@ -61,17 +140,27 @@ log_error() { echo -e "${RED}❌ $1${NC}" >&2 } +# Flag to indicate if we started services (for cleanup) +SERVICES_STARTED=false + # Cleanup function cleanup() { local exit_code=$? - if [ $exit_code -ne 0 ]; then - log_error "Script failed. Cleaning up..." + + # Only cleanup if services were started + if [ "$SERVICES_STARTED" = "true" ]; then + log_info "Cleaning up..." + # Stop Docker services docker compose -f "${COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true + # Stop Anvil if [ -n "${ANVIL_PID:-}" ]; then kill $ANVIL_PID 2>/dev/null || true fi + # Also kill any running anvil processes on port 8545 + pkill -f "anvil.*8545" 2>/dev/null || true + # Stop price updater if [ -n "${PRICE_UPDATER_PID:-}" ]; then kill $PRICE_UPDATER_PID 2>/dev/null || true @@ -81,12 +170,37 @@ cleanup() { kill $(cat "${ROOT_DIR}/.temp/price-updater.pid") 2>/dev/null || true rm -f "${ROOT_DIR}/.temp/price-updater.pid" fi + + if [ $exit_code -ne 0 ]; then + log_error "Script exited with error code: $exit_code" + else + log_success "Cleanup complete" + fi fi exit $exit_code } trap cleanup EXIT INT TERM +# Stop command for easy cleanup +stop_all() { + log_info "Stopping all services..." + + # Stop Docker services + docker compose -f "${COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true + + # Stop Anvil + pkill -f "anvil.*8545" 2>/dev/null || true + + # Stop price updater + if [ -f "${ROOT_DIR}/.temp/price-updater.pid" ]; then + kill $(cat "${ROOT_DIR}/.temp/price-updater.pid") 2>/dev/null || true + rm -f "${ROOT_DIR}/.temp/price-updater.pid" + fi + + log_success "All services stopped" +} + # Step 1: Start Anvil @@ -97,8 +211,9 @@ start_anvil() { pkill -f "anvil.*8545" || true sleep 2 - # Start anvil in background - anvil --host 0.0.0.0 --port 8545 --chain-id 31337 --balance 10000 & + # Start anvil in background with WebSocket support + # --ipc enables IPC, which is needed for WebSocket subscriptions to work + anvil --host 0.0.0.0 --port 8545 --chain-id 31337 --balance 10000 --ipc & ANVIL_PID=$! # Wait for anvil to be ready @@ -138,6 +253,8 @@ deploy_all_contracts() { deploy_registry deploy_protocol_fee_hook deploy_receiver + deploy_dia_oracle_randomness + deploy_random_request_manager configure_contracts fund_receiver initialize_oracle_prices @@ -233,6 +350,8 @@ deploy_receiver() { local output cd "${CONTRACTS_DIR}" + # IMPORTANT: Use same domain parameters as OracleIntentRegistry for EIP-712 signature verification + # Domain: "DIA Oracle" version "1.0", chainId 31337, verifyingContract = registry_addr if ! output=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge create \ --rpc-url "${ANVIL_RPC}" \ --private-key "${DEFAULT_KEY}" \ @@ -254,6 +373,88 @@ deploy_receiver() { echo "$address" > "${RECEIVER_ADDR_FILE}" log_success "PushOracleReceiverV2 deployed at $address" + + # Verify domain separator matches by getting it from both contracts + log_info "Verifying domain separator consistency..." + local registry_domain receiver_domain + + registry_domain=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast call \ + --rpc-url "${ANVIL_RPC}" \ + "$registry_addr" \ + "domainSeparator()(bytes32)" 2>/dev/null || echo "") + + receiver_domain=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast call \ + --rpc-url "${ANVIL_RPC}" \ + "$address" \ + "domainSeparator()(bytes32)" 2>/dev/null || echo "") + + if [ -n "$registry_domain" ] && [ -n "$receiver_domain" ]; then + if [ "$registry_domain" = "$receiver_domain" ]; then + log_success "✅ Domain separators match: $registry_domain" + else + log_error "❌ Domain separator mismatch!" + log_error " Registry: $registry_domain" + log_error " Receiver: $receiver_domain" + log_error " Signatures will fail - intents cannot be verified!" + return 1 + fi + else + log_warning "⚠️ Could not verify domain separators (contracts may not expose domainSeparator())" + fi +} + +# Deploy DIAOracleRandomness +deploy_dia_oracle_randomness() { + log_info "🚀 Deploying DIAOracleRandomness..." + local output + cd "${CONTRACTS_DIR}" + if ! output=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge create \ + --rpc-url "${ANVIL_RPC}" \ + --private-key "${DEFAULT_KEY}" \ + --broadcast \ + "contracts/VRF/DIAOracleRandomness.sol:DIAOracleRandomness" 2>&1); then + log_error "Failed to deploy DIAOracleRandomness" + echo "$output" >&2 + return 1 + fi + + local address + address=$(echo "$output" | awk '/Deployed to:/ {print $3}') + if [ -z "$address" ]; then + log_error "Failed to capture DIAOracleRandomness address" + echo "$output" >&2 + return 1 + fi + + echo "$address" > "${DIA_ORACLE_RANDOMNESS_ADDR_FILE}" + log_success "DIAOracleRandomness deployed at $address" +} + +# Deploy RandomRequestManager +deploy_random_request_manager() { + log_info "🚀 Deploying RandomRequestManager..." + local output + cd "${CONTRACTS_DIR}" + if ! output=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge create \ + --rpc-url "${ANVIL_RPC}" \ + --private-key "${DEFAULT_KEY}" \ + --broadcast \ + "contracts/VRF/RandomRequestManager.sol:RandomRequestManager" 2>&1); then + log_error "Failed to deploy RandomRequestManager" + echo "$output" >&2 + return 1 + fi + + local address + address=$(echo "$output" | awk '/Deployed to:/ {print $3}') + if [ -z "$address" ]; then + log_error "Failed to capture RandomRequestManager address" + echo "$output" >&2 + return 1 + fi + + echo "$address" > "${RANDOM_REQUEST_MANAGER_ADDR_FILE}" + log_success "RandomRequestManager deployed at $address" } # Configure contracts @@ -314,17 +515,17 @@ fund_receiver() { local receiver_addr receiver_addr=$(cat "${RECEIVER_ADDR_FILE}") - # Fund the receiver with 1 ETH + # Fund the receiver with 100 ETH for extensive testing if ! FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send \ --rpc-url "${ANVIL_RPC}" \ --private-key "${DEFAULT_KEY}" \ - --value "1ether" \ + --value "100ether" \ "$receiver_addr"; then log_error "Failed to fund PushOracleReceiverV2" return 1 fi - log_success "PushOracleReceiverV2 funded with 1 ETH" + log_success "PushOracleReceiverV2 funded with 100 ETH" } # Initialize oracle with initial prices @@ -410,16 +611,19 @@ create_wallets() { create_attestor_env() { log_info "Creating attestor environment configuration..." cat < "${CONFIG_DIR}/attestor.env" -RPC_URLS=http://host.docker.internal:8545 -PRIVATE_KEY=${DEFAULT_KEY} -INTENT_REGISTRY_ADDRESS=$(cat "${REGISTRY_ADDR_FILE}") -SYMBOLS=BTC/USD,ETH/USD -POLLING_TIME=5s -BATCH_MODE=false -INTENT_TYPE=OracleUpdate -INTENT_VERSION=1.0 -METRICS_PORT=8080 -API_PORT=8081 +ATTESTOR_RPC_URLS=http://host.docker.internal:8545 +ATTESTOR_ORACLE_ADDRESS=$(cat "${DIA_ORACLE_ADDR_FILE}") +ATTESTOR_ORACLE_CLIENT_TYPE=dia_v2 +ATTESTOR_REGISTRY_ADDRESS=$(cat "${REGISTRY_ADDR_FILE}") +ATTESTOR_ATTESTOR_PRIVATE_KEY=${DEFAULT_KEY} +ATTESTOR_ATTESTOR_SYMBOLS=BTC/USD,ETH/USD +ATTESTOR_ATTESTOR_POLLING_TIME=5s +ATTESTOR_ATTESTOR_BATCH_MODE=false +ATTESTOR_ATTESTOR_MODE=prime +ATTESTOR_ATTESTOR_INTENT_TYPE=OracleUpdate +ATTESTOR_ATTESTOR_INTENT_VERSION=1.0 +ATTESTOR_METRICS_PORT=8080 +ATTESTOR_API_PORT=8081 ENV # Create config.yaml for local development @@ -432,10 +636,13 @@ rpc: urls: - http://host.docker.internal:8545 registry_url: http://host.docker.internal:8545 + registry_urls: + - http://host.docker.internal:8545 # Oracle Configuration oracle: address: "$(cat "${DIA_ORACLE_ADDR_FILE}")" + client_type: "dia_v2" # Registry Configuration registry: @@ -443,17 +650,17 @@ registry: # Attestor Configuration attestor: + private_key: "${DEFAULT_KEY}" symbols: - BTC/USD - ETH/USD polling_time: 5s batch_mode: false - intent_type: OracleUpdate - intent_version: "1.0" + mode: prime # Logging Configuration logging: - level: info + level: debug # Metrics Configuration metrics: @@ -565,6 +772,22 @@ contracts: fieldsmapping: intent: fullIntent gaslimit: 300000 + random_request_manager: + chain_id: 31337 + address: $(cat "${RANDOM_REQUEST_MANAGER_ADDR_FILE}") + type: randomness + enabled: true + abi: '[{"name":"fulfillRandomInt","type":"function","inputs":[{"name":"requestId","type":"uint256"},{"name":"randomInts","type":"int256[]"}]}]' + gas_limit: 500000 + gas_multiplier: 1.2 + max_gas_price: "100000000000" + methods: + fulfill_random: + methodname: fulfillRandomInt + fieldsmapping: + requestId: requestId + randomInts: randomInts + gaslimit: 500000 YAML # 4. Create events.yaml @@ -587,6 +810,25 @@ event_definitions: - \${event.intentHash} returns: fullIntent: "0" + IntArraySet: + contract: $(cat "${DIA_ORACLE_RANDOMNESS_ADDR_FILE}") + abi: '{"name":"IntArraySet","type":"event","inputs":[{"name":"requestId","type":"uint256","indexed":false},{"name":"round","type":"int256","indexed":true},{"name":"seed","type":"string","indexed":false},{"name":"signature","type":"string","indexed":false}]}' + dataextraction: + requestId: requestId + round: topics[1] + seed: seed + signature: signature + enrichment: + contract: "" + method: getIntArray + abi: '{"name":"getIntArray","type":"function","inputs":[{"name":"requestId_","type":"uint256"}],"outputs":[{"name":"requestId","type":"uint256"},{"name":"randomInts","type":"int256[]"},{"name":"round","type":"int64"},{"name":"seed","type":"string"},{"name":"signature","type":"string"}]}' + params: + - \${event.requestId} + returns: + randomInts: "1" + fullRound: "2" + fullSeed: "3" + fullSignature: "4" YAML # 5. Create router configs @@ -595,25 +837,29 @@ YAML "oracle_intent_router_eth.yaml" "oracle_intent_router_sol.yaml" "oracle_intent_router.yaml" + "randomness_router.yaml" ) local router_names=( "oracle_intent_router_btc" "oracle_intent_router_eth" "oracle_intent_router_sol" "oracle_intent_router" + "randomness_router_local" ) local router_ids=( "oracle_intent_router_btc_001" "oracle_intent_router_eth_001" "oracle_intent_router_sol_001" "oracle_intent_router_001" + "randomness_router_001" ) - local router_thresholds=("1s" "1s" "1s" "2s") + local router_thresholds=("1s" "1s" "1s" "2s" "1s") local router_conditions=( - $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: ==\n value: BTC/USD\n' - $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: ==\n value: ETH/USD\n' - $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: ==\n value: SOL/USD\n' - $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: !=\n value: BTC/USD\n - field: ${enrichment.fullIntent.Symbol}\n operator: !=\n value: ETH/USD\n - field: ${enrichment.fullIntent.Symbol}\n operator: !=\n value: SOL/USD\n' + $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: eq\n value: BTC/USD\n' + $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: eq\n value: ETH/USD\n' + $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: eq\n value: SOL/USD\n' + $' conditions:\n - field: ${enrichment.fullIntent.Symbol}\n operator: ne\n value: BTC/USD\n - field: ${enrichment.fullIntent.Symbol}\n operator: ne\n value: ETH/USD\n - field: ${enrichment.fullIntent.Symbol}\n operator: ne\n value: SOL/USD\n' + $' conditions: []\n' ) for ((i = 0; i < ${#router_files[@]}; i++)); do @@ -623,7 +869,38 @@ YAML local time_threshold="${router_thresholds[$i]}" local conditions_block="${router_conditions[$i]}" - cat < "${file_path}" + # Special handling for randomness router + if [ "$router_name" = "randomness_router_local" ]; then + cat < "${file_path}" +router: + id: ${router_id} + name: ${router_name} + type: event + enabled: true + private_key_env: PRIVATE_KEY + triggers: + events: + - IntArraySet +${conditions_block} + processing: + datasource: enrichment + transformations: [] + validationenabled: true + destinations: + - contract_ref: random_request_manager + time_threshold: ${time_threshold} + method: + name: fulfillRandomInt + abi: '{"name":"fulfillRandomInt","type":"function","inputs":[{"name":"requestId","type":"uint256"},{"name":"randomInts","type":"int256[]"}]}' + params: + requestId: \${event.requestId} + randomInts: \${enrichment.randomInts} + value: "0" + gaslimit: 500000 + gasmultiplier: 1.2 +YAML + else + cat < "${file_path}" router: id: ${router_id} name: ${router_name} @@ -641,6 +918,7 @@ ${conditions_block} destinations: - contract_ref: push_oracle_receiver time_threshold: ${time_threshold} + price_deviation: "0.5%" method: name: handleIntentUpdate abi: '{"name":"handleIntentUpdate","type":"function","inputs":[{"name":"intent","type":"tuple","components":[{"name":"intentType","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"expiry","type":"uint256"},{"name":"symbol","type":"string"},{"name":"price","type":"uint256"},{"name":"timestamp","type":"uint256"},{"name":"source","type":"string"},{"name":"signature","type":"bytes"},{"name":"signer","type":"address"}]}]}' @@ -650,6 +928,7 @@ ${conditions_block} gaslimit: 300000 gasmultiplier: 1.2 YAML + fi done log_success "Bridge modular YAML configuration created at ${BRIDGE_CONFIG_DIR} with ${#router_files[@]} routers" @@ -679,7 +958,7 @@ start_docker_services() { fi log_success "Docker images built successfully" - # Start only the services (not anvil since we have it running on host) + # Start all services (not anvil since we have it running on host) if ! docker compose -f "${COMPOSE_FILE}" up -d postgres attestor bridge; then log_error "Failed to start Docker services" return 1 @@ -688,28 +967,130 @@ start_docker_services() { log_success "Docker services started successfully" } -# Wait for services to be healthy +# Wait for services to be healthy with proper health checks wait_for_services() { - log_info "Waiting for services to start..." - sleep 5 + log_info "Waiting for services to be healthy..." + + # Wait for Postgres + log_info "Waiting for Postgres..." + local postgres_ready=false + for i in {1..30}; do + if docker exec spectra-interoperability-postgres-1 pg_isready -U bridge &>/dev/null; then + postgres_ready=true + break + fi + sleep 1 + done + if $postgres_ready; then + log_success "Postgres is healthy" + else + log_error "Postgres failed to become healthy" + docker logs spectra-interoperability-postgres-1 --tail 20 2>/dev/null || true + return 1 + fi + + # Wait for Attestor + log_info "Waiting for Attestor..." + local attestor_ready=false + for i in {1..60}; do + if curl -s http://localhost:8081/health &>/dev/null; then + attestor_ready=true + break + fi + # Check if container is still running + if ! docker ps --filter "name=spectra-interoperability-attestor-1" --filter "status=running" | grep -q attestor; then + log_error "Attestor container exited unexpectedly" + docker logs spectra-interoperability-attestor-1 --tail 30 2>/dev/null || true + return 1 + fi + sleep 2 + done + if $attestor_ready; then + log_success "Attestor is healthy" + else + log_warning "Attestor health endpoint not responding (may still be starting)" + docker logs spectra-interoperability-attestor-1 --tail 10 2>/dev/null || true + fi - # Check if services are running - if docker ps --filter "name=spectra-interoperability-postgres-1" --filter "status=running" | grep -q postgres; then - log_success "Postgres service is running" + # Wait for Bridge + log_info "Waiting for Bridge..." + local bridge_ready=false + for i in {1..60}; do + if curl -s http://localhost:8082/metrics &>/dev/null; then + bridge_ready=true + break + fi + # Check if container is still running + if ! docker ps --filter "name=spectra-interoperability-bridge-1" --filter "status=running" | grep -q bridge; then + log_error "Bridge container exited unexpectedly" + docker logs spectra-interoperability-bridge-1 --tail 30 2>/dev/null || true + return 1 + fi + sleep 2 + done + if $bridge_ready; then + log_success "Bridge is healthy" else - log_warning "Postgres service may still be starting" + log_warning "Bridge metrics endpoint not responding (may still be starting)" + docker logs spectra-interoperability-bridge-1 --tail 10 2>/dev/null || true fi - if docker ps --filter "name=spectra-interoperability-attestor-1" --filter "status=running" | grep -q attestor; then - log_success "Attestor service is running" + echo "" + log_success "All core services are running!" +} + +# Health check function to verify system is operational +run_health_checks() { + log_info "Running system health checks..." + local all_healthy=true + + # Check Anvil RPC + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$ANVIL_RPC" &>/dev/null; then + log_success "Anvil RPC: OK" else - log_warning "Attestor service may still be starting" + log_error "Anvil RPC: FAILED" + all_healthy=false fi - if docker ps --filter "name=spectra-interoperability-bridge-1" --filter "status=running" | grep -q bridge; then - log_success "Bridge service is running" + # Check Postgres + if docker exec spectra-interoperability-postgres-1 pg_isready -U bridge &>/dev/null; then + log_success "Postgres: OK" else - log_warning "Bridge service may still be starting" + log_error "Postgres: FAILED" + all_healthy=false + fi + + # Check contract deployments + local registry_addr=$(cat "${REGISTRY_ADDR_FILE}" 2>/dev/null || echo "") + if [ -n "$registry_addr" ]; then + local code=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast code --rpc-url "$ANVIL_RPC" "$registry_addr" 2>/dev/null || echo "0x") + if [ "$code" != "0x" ] && [ -n "$code" ]; then + log_success "Contracts deployed: OK" + else + log_error "Contracts: No code at registry address" + all_healthy=false + fi + else + log_error "Contracts: Registry address file not found" + all_healthy=false + fi + + # Check Docker services + local services=("postgres" "attestor" "bridge") + for svc in "${services[@]}"; do + if docker ps --filter "name=spectra-interoperability-${svc}-1" --filter "status=running" | grep -q "$svc"; then + log_success "Docker $svc: Running" + else + log_warning "Docker $svc: Not running" + fi + done + + if $all_healthy; then + log_success "All health checks passed!" + else + log_warning "Some health checks failed" fi } @@ -741,10 +1122,26 @@ show_summary() { echo " 🏭 OracleIntentRegistry: $(cat "${REGISTRY_ADDR_FILE}")" echo " 💰 ProtocolFeeHook: $(cat "${PROTOCOL_FEE_HOOK_FILE}")" echo " 📡 PushOracleReceiverV2: $(cat "${RECEIVER_ADDR_FILE}")" - echo " 💰 Receiver Balance: 1 ETH" + echo " 🎲 DIAOracleRandomness: $(cat "${DIA_ORACLE_RANDOMNESS_ADDR_FILE}")" + echo " 🎯 RandomRequestManager: $(cat "${RANDOM_REQUEST_MANAGER_ADDR_FILE}")" + echo " 💰 Receiver Balance: 100 ETH" echo " 🔑 Authorized Signer: $DEFAULT_ADDRESS" echo " 🗄️ Postgres DSN: ${POSTGRES_DSN}" echo "" + echo "🔐 EIP-712 Domain Configuration:" + local registry_addr receiver_addr registry_domain + registry_addr=$(cat "${REGISTRY_ADDR_FILE}") + receiver_addr=$(cat "${RECEIVER_ADDR_FILE}") + registry_domain=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast call \ + --rpc-url "${ANVIL_RPC}" \ + "$registry_addr" \ + "domainSeparator()(bytes32)" 2>/dev/null || echo "N/A") + echo " 📝 Domain Name: DIA Oracle" + echo " 🔢 Domain Version: 1.0" + echo " 🔗 Chain ID: 31337" + echo " ✍️ Verifying Contract: $registry_addr (OracleIntentRegistry)" + echo " 🔑 Domain Separator: $registry_domain" + echo "" echo "🔧 Configuration Files:" echo " ⚖️ Attestor env: ${CONFIG_DIR}/attestor.env" echo " 📋 Attestor config: ${CONFIG_DIR}/attestor-local.yaml" @@ -757,7 +1154,8 @@ show_summary() { echo " ├── oracle_intent_router_btc.yaml" echo " ├── oracle_intent_router_eth.yaml" echo " ├── oracle_intent_router_sol.yaml" - echo " └── oracle_intent_router.yaml" + echo " ├── oracle_intent_router.yaml" + echo " └── randomness_router.yaml" echo " 📄 Contract addresses: ${CONTRACTS_ADDR_DIR}/" echo " 🔑 Wallets: ${WALLETS_DIR}/" echo "" @@ -769,15 +1167,20 @@ show_summary() { echo " ⛏️ Anvil RPC: ${ANVIL_RPC}" echo " 📊 Attestor metrics: http://localhost:8080/metrics" echo " 🔍 Attestor API: http://localhost:8081/health" + echo " 🔍 Bridge metrics: http://localhost:8082/metrics" echo "" echo "📈 Oracle Information:" echo " 🔄 Price updates every 10 seconds (ETH/USD & BTC/USD)" echo " 📊 Price updater log: ${ROOT_DIR}/.temp/price-updater.log" echo "" echo "🌉 Bridge Router Configuration:" - echo " 📝 Event: IntentRegistered from OracleIntentRegistry" - echo " 🎯 Destination: PushOracleReceiverV2.handleIntentUpdate()" - echo " 🧭 Routers: BTC, ETH, SOL dedicated routers plus a fallback router" + echo " 📝 Events:" + echo " - IntentRegistered from OracleIntentRegistry" + echo " - IntArraySet from DIAOracleRandomness" + echo " 🎯 Destinations:" + echo " - PushOracleReceiverV2.handleIntentUpdate()" + echo " - RandomRequestManager.fulfillRandomInt()" + echo " 🧭 Routers: BTC, ETH, SOL dedicated routers, fallback router, and randomness router" echo " 🔐 Router Signer: $DEFAULT_ADDRESS (authorized in registry)" echo "" log_info "Anvil is running with PID: $ANVIL_PID" @@ -787,36 +1190,102 @@ show_summary() { log_info "Press Ctrl+C to stop everything and exit" } -# Main execution -main() { - log_info "🚀 Starting Spectra Local Development Environment" +# Show usage +show_usage() { + echo "Usage: $0 [command]" echo "" + echo "Commands:" + echo " start Start the local development environment (default)" + echo " stop Stop all running services" + echo " status Show status of all services" + echo " logs Show logs from Docker services" + echo " help Show this help message" +} +# Show status of services +show_status() { + log_info "Service Status:" + echo "" + + # Check Anvil + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$ANVIL_RPC" &>/dev/null; then + log_success "Anvil: Running at $ANVIL_RPC" + else + log_error "Anvil: Not running" + fi + + # Check Docker services + echo "" + docker compose -f "${COMPOSE_FILE}" ps 2>/dev/null || log_warning "Docker compose not running" +} +# Show logs +show_logs() { + docker compose -f "${COMPOSE_FILE}" logs -f --tail 100 +} - # Step 1: Start Anvil blockchain - start_anvil - - # Step 2: Set up contracts and deploy - setup_and_deploy_contracts - - # Step 3: Create wallets and configurations - create_wallets_and_configs - - # Step 4: Start Docker services - start_docker_services - - # Wait for services to be healthy - wait_for_services - - # Start price updater - start_price_updater - - # Show summary - show_summary - - # Keep script running (Anvil in foreground) - wait $ANVIL_PID +# Main execution +main() { + local command="${1:-start}" + + case "$command" in + start) + log_info "🚀 Starting Spectra Local Development Environment" + echo "" + + # Mark that we're starting services (for cleanup) + SERVICES_STARTED=true + + # Step 0: Check dependencies + check_dependencies + + # Step 1: Start Anvil blockchain + start_anvil + + # Step 2: Set up contracts and deploy + setup_and_deploy_contracts + + # Step 3: Create wallets and configurations + create_wallets_and_configs + + # Step 4: Start Docker services + start_docker_services + + # Wait for services to be healthy + wait_for_services + + # Run health checks + run_health_checks + + # Start price updater + start_price_updater + + # Show summary + show_summary + + # Keep script running (Anvil in foreground) + wait $ANVIL_PID + ;; + stop) + stop_all + ;; + status) + show_status + ;; + logs) + show_logs + ;; + help|--help|-h) + show_usage + ;; + *) + log_error "Unknown command: $command" + show_usage + exit 1 + ;; + esac } main "$@" From 5107ec211774141b885226e7896f85e2804fb896 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 30 Jan 2026 10:39:02 +0530 Subject: [PATCH 4/8] chore: sript to start multichain test locally --- scripts/monitor-multichain.sh | 348 ++++++++++++ scripts/monitor-receiver.sh | 292 +++++++--- scripts/start-multichain.sh | 972 ++++++++++++++++++++++++++++++++++ 3 files changed, 1526 insertions(+), 86 deletions(-) create mode 100755 scripts/monitor-multichain.sh create mode 100755 scripts/start-multichain.sh diff --git a/scripts/monitor-multichain.sh b/scripts/monitor-multichain.sh new file mode 100755 index 0000000..144683d --- /dev/null +++ b/scripts/monitor-multichain.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env bash +set -euo pipefail + +####################################### +# Multi-Chain PushOracleReceiverV2 Monitor +####################################### + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MULTICHAIN_DIR="${ROOT_DIR}/.local-stack/multichain" +LOG_DIR="${ROOT_DIR}/.local-stack/logs" +LOG_FILE="${LOG_DIR}/multichain-monitor.log" +STATE_FILE="${LOG_DIR}/multichain-state.json" + +# Chain configuration (must match start-multichain.sh) +CHAINS=( + "31337:8545:anvil-main:2" + "31338:8546:anvil-eth:1" + "31339:8547:anvil-arb:2" + "31340:8548:anvil-opt:1" + "31341:8549:anvil-base:1" + "31342:8550:anvil-poly:1" + "31343:8551:anvil-avax:1" + "31344:8552:anvil-bsc:1" + "31345:8553:anvil-ftm:1" + "31346:8554:anvil-zksync:1" +) + +SYMBOLS=("ETH/USD" "BTC/USD") +POLL_INTERVAL="${POLL_INTERVAL:-10}" +DEVIATION_THRESHOLD="${DEVIATION_THRESHOLD:-0.5}" + +####################################### +# Logging +####################################### + +timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } + +log_to_file() { + echo "[$(timestamp)] [$1] $2" >> "$LOG_FILE" +} + +log_info() { + echo -e "${BLUE}[$(timestamp)]${NC} ${CYAN}[INFO]${NC} $1" + log_to_file "INFO" "$1" +} + +log_update() { + echo -e "${BLUE}[$(timestamp)]${NC} ${GREEN}[UPDATE]${NC} $1" + log_to_file "UPDATE" "$1" +} + +log_deviation() { + echo -e "${BLUE}[$(timestamp)]${NC} ${YELLOW}[DEVIATION]${NC} $1" + log_to_file "DEVIATION" "$1" +} + +log_error() { + echo -e "${BLUE}[$(timestamp)]${NC} ${RED}[ERROR]${NC} $1" + log_to_file "ERROR" "$1" +} + +####################################### +# Query Functions +####################################### + +get_price() { + local rpc="$1" + local contract="$2" + local symbol="$3" + + local result=$(cast call --rpc-url "$rpc" "$contract" \ + "getValue(string)(uint128,uint128)" "$symbol" 2>/dev/null || echo "0\n0") + + python3 -c " +import re +raw = '''$result''' +lines = raw.strip().split('\n') +val, ts = 0, 0 +try: + if len(lines) >= 2: + val_match = re.match(r'^(\d+)', lines[0].strip()) + ts_match = re.match(r'^(\d+)', lines[1].strip()) + if val_match: val = int(val_match.group(1)) + if ts_match: ts = int(ts_match.group(1)) +except: pass +print(f'{val} {ts}') +" 2>/dev/null || echo "0 0" +} + +wei_to_price() { + python3 -c "print(f'{int(\"${1:-0}\") / 1e18:.2f}')" 2>/dev/null || echo "0.00" +} + +####################################### +# Status Display +####################################### + +print_status() { + clear + echo "" + echo "═══════════════════════════════════════════════════════════════════════════════════" + echo " Multi-Chain PushOracleReceiverV2 Monitor - $(timestamp)" + echo "═══════════════════════════════════════════════════════════════════════════════════" + echo "" + + local now=$(date +%s) + local total_receivers=0 + local active_receivers=0 + + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + local rpc="http://localhost:$port" + local chain_dir="${MULTICHAIN_DIR}/${chain_id}" + + # Check if chain is running + local chain_status="${RED}OFFLINE${NC}" + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$rpc" &>/dev/null; then + chain_status="${GREEN}ONLINE${NC}" + fi + + echo -e " ┌─ ${CYAN}Chain $chain_id${NC} ($name) - $chain_status" + echo -e " │ RPC: $rpc" + + # Get source oracle price + local oracle_addr=$(cat "${chain_dir}/dia_oracle_v2.addr" 2>/dev/null || echo "") + local source_eth="" source_btc="" + + if [ -n "$oracle_addr" ]; then + local src_result=$(get_price "$rpc" "$oracle_addr" "ETH/USD") + source_eth=$(echo "$src_result" | awk '{print $1}') + src_result=$(get_price "$rpc" "$oracle_addr" "BTC/USD") + source_btc=$(echo "$src_result" | awk '{print $1}') + fi + + # Check each receiver + for i in $(seq 1 "$num_receivers"); do + local receiver_file="${chain_dir}/push_oracle_receiver_v2_${i}.addr" + total_receivers=$((total_receivers + 1)) + + if [ -f "$receiver_file" ]; then + local receiver=$(cat "$receiver_file") + local short_addr="${receiver:0:10}...${receiver: -6}" + + echo " │" + echo -e " │ ${YELLOW}Receiver #$i${NC}: $short_addr" + + for symbol in "${SYMBOLS[@]}"; do + local result=$(get_price "$rpc" "$receiver" "$symbol") + local value=$(echo "$result" | awk '{print $1}') + local ts=$(echo "$result" | awk '{print $2}') + local price=$(wei_to_price "$value") + + # Calculate age + local age="N/A" + if [ "$ts" != "0" ] && [ -n "$ts" ]; then + local age_sec=$((now - ts)) + if [ "$age_sec" -lt 60 ]; then + age="${age_sec}s" + elif [ "$age_sec" -lt 3600 ]; then + age="$((age_sec / 60))m" + else + age="$((age_sec / 3600))h" + fi + active_receivers=$((active_receivers + 1)) + fi + + # Calculate deviation from source + local deviation="N/A" + local source_val="" + if [ "$symbol" = "ETH/USD" ]; then + source_val="$source_eth" + else + source_val="$source_btc" + fi + + if [ -n "$source_val" ] && [ "$source_val" != "0" ] && [ "$value" != "0" ]; then + deviation=$(python3 -c " +src = int('$source_val') +rcv = int('$value') +if rcv > 0: + dev = abs(src - rcv) / rcv * 100 + print(f'{dev:.2f}%') +else: + print('N/A') +" 2>/dev/null || echo "N/A") + fi + + # Color code based on status + local status_color="$GREEN" + if [ "$age" = "N/A" ]; then + status_color="$RED" + elif [ "${deviation%\%}" != "N/A" ]; then + local dev_num=$(echo "$deviation" | tr -d '%') + if (( $(echo "$dev_num > $DEVIATION_THRESHOLD" | bc -l 2>/dev/null || echo 0) )); then + status_color="$YELLOW" + fi + fi + + printf " │ %-8s \$%-10s Age: %-6s Dev: %s\n" \ + "$symbol" "$price" "$age" "$deviation" + done + fi + done + echo " └─" + echo "" + done + + echo "═══════════════════════════════════════════════════════════════════════════════════" + echo " Summary: $active_receivers/$((total_receivers * 2)) price feeds active across $total_receivers receivers" + echo " Log: $LOG_FILE" + echo " Press Ctrl+C to exit" + echo "═══════════════════════════════════════════════════════════════════════════════════" +} + +####################################### +# Continuous Monitoring +####################################### + +monitor_loop() { + mkdir -p "$LOG_DIR" + log_info "Starting multi-chain monitor..." + log_info "Watching ${#CHAINS[@]} chains, $POLL_INTERVAL second interval" + + while true; do + print_status + sleep "$POLL_INTERVAL" + done +} + +####################################### +# JSON Status Export +####################################### + +export_status() { + mkdir -p "$LOG_DIR" + + python3 -c " +import json +from datetime import datetime +import subprocess +import re + +chains = '''${CHAINS[*]}'''.split() +multichain_dir = '$MULTICHAIN_DIR' +now = int(datetime.now().timestamp()) + +status = { + 'timestamp': datetime.utcnow().isoformat() + 'Z', + 'chains': {} +} + +for chain_config in chains: + parts = chain_config.split(':') + chain_id, port, name, num_receivers = parts[0], parts[1], parts[2], int(parts[3]) + + chain_status = { + 'name': name, + 'port': int(port), + 'rpc': f'http://localhost:{port}', + 'receivers': [] + } + + chain_dir = f'{multichain_dir}/{chain_id}' + + for i in range(1, num_receivers + 1): + receiver_file = f'{chain_dir}/push_oracle_receiver_v2_{i}.addr' + try: + with open(receiver_file) as f: + addr = f.read().strip() + chain_status['receivers'].append({ + 'id': i, + 'address': addr + }) + except: + pass + + status['chains'][chain_id] = chain_status + +print(json.dumps(status, indent=2)) +" > "$STATE_FILE" + + echo "Status exported to: $STATE_FILE" + cat "$STATE_FILE" +} + +####################################### +# Main +####################################### + +show_help() { + echo "Multi-Chain PushOracleReceiverV2 Monitor" + echo "" + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " start Start continuous monitoring (default)" + echo " status Show current status once" + echo " export Export status to JSON" + echo " logs Show recent log entries" + echo " help Show this help" + echo "" + echo "Environment Variables:" + echo " POLL_INTERVAL Seconds between updates (default: 10)" + echo " DEVIATION_THRESHOLD Alert threshold % (default: 0.5)" +} + +main() { + local command="${1:-start}" + + case "$command" in + start) + monitor_loop + ;; + status) + print_status + ;; + export) + export_status + ;; + logs) + if [ -f "$LOG_FILE" ]; then + tail -50 "$LOG_FILE" + else + echo "No log file found" + fi + ;; + help|--help|-h) + show_help + ;; + *) + echo "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/monitor-receiver.sh b/scripts/monitor-receiver.sh index ff93f2d..001a734 100755 --- a/scripts/monitor-receiver.sh +++ b/scripts/monitor-receiver.sh @@ -2,8 +2,8 @@ set -euo pipefail ####################################### -# PushOracleReceiverV2 Monitor -# Monitors oracle updates and logs: +# PushOracleReceiverV2 Multi-Chain Monitor +# Monitors ALL chains and ALL receivers: # - Received updates # - Expected updates (based on deviation) # - Missed updates @@ -15,10 +15,8 @@ LOG_DIR="${ROOT_DIR}/.local-stack/logs" LOG_FILE="${LOG_DIR}/receiver-monitor.log" STATE_FILE="${LOG_DIR}/receiver-state.json" METRICS_FILE="${LOG_DIR}/receiver-metrics.json" - -RPC_URL="${RPC_URL:-http://localhost:8545}" -RECEIVER_ADDR_FILE="${ROOT_DIR}/.local-stack/contracts/push_oracle_receiver_v2.addr" -DIA_ORACLE_ADDR_FILE="${ROOT_DIR}/.local-stack/contracts/dia_oracle_v2.addr" +MULTICHAIN_DIR="${ROOT_DIR}/.local-stack/multichain" +CONTRACTS_DIR="${ROOT_DIR}/.local-stack/contracts" # Monitoring parameters POLL_INTERVAL="${POLL_INTERVAL:-5}" # Seconds between checks @@ -34,27 +32,73 @@ BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' +# Store discovered receivers: "chain_id:receiver_num:address" +RECEIVERS=() +DIA_ORACLE_ADDR="" +SOURCE_CHAIN_ID="" + +# Get port for a chain ID +get_chain_port() { + local chain_id="$1" + # Ports start at 8545 for chain 31337 + echo $((8545 + chain_id - 31337)) +} + ####################################### # Initialization ####################################### -init() { - mkdir -p "$LOG_DIR" +discover_receivers() { + RECEIVERS=() - # Check if receiver address exists - if [ ! -f "$RECEIVER_ADDR_FILE" ]; then - echo "Error: Receiver address file not found: $RECEIVER_ADDR_FILE" - echo "Run ./scripts/start-local.sh first" - exit 1 + # Check for multichain setup first + if [ -d "$MULTICHAIN_DIR" ]; then + for chain_dir in "$MULTICHAIN_DIR"/*/; do + if [ -d "$chain_dir" ]; then + local chain_id=$(basename "$chain_dir") + + # Find DIA Oracle on source chain (31337) + if [ "$chain_id" = "31337" ] && [ -f "${chain_dir}/dia_oracle_v2.addr" ]; then + DIA_ORACLE_ADDR=$(cat "${chain_dir}/dia_oracle_v2.addr") + SOURCE_CHAIN_ID="31337" + fi + + # Find all receivers on this chain + shopt -s nullglob + for receiver_file in "${chain_dir}"/push_oracle_receiver_v2_*.addr; do + if [ -f "$receiver_file" ]; then + local receiver_num=$(echo "$receiver_file" | sed 's/.*push_oracle_receiver_v2_\([0-9]*\)\.addr/\1/') + local receiver_addr=$(cat "$receiver_file") + RECEIVERS+=("${chain_id}:${receiver_num}:${receiver_addr}") + fi + done + shopt -u nullglob + fi + done fi - RECEIVER_ADDR=$(cat "$RECEIVER_ADDR_FILE") + # Fallback to single-chain setup + if [ ${#RECEIVERS[@]} -eq 0 ]; then + if [ -f "${CONTRACTS_DIR}/push_oracle_receiver_v2.addr" ]; then + local receiver_addr=$(cat "${CONTRACTS_DIR}/push_oracle_receiver_v2.addr") + RECEIVERS+=("31337:1:${receiver_addr}") + SOURCE_CHAIN_ID="31337" + fi + if [ -f "${CONTRACTS_DIR}/dia_oracle_v2.addr" ]; then + DIA_ORACLE_ADDR=$(cat "${CONTRACTS_DIR}/dia_oracle_v2.addr") + fi + fi +} + +init() { + mkdir -p "$LOG_DIR" + + discover_receivers - # Check if DIA oracle address exists - if [ -f "$DIA_ORACLE_ADDR_FILE" ]; then - DIA_ORACLE_ADDR=$(cat "$DIA_ORACLE_ADDR_FILE") - else - DIA_ORACLE_ADDR="" + if [ ${#RECEIVERS[@]} -eq 0 ]; then + echo "Error: No receivers found" + echo "Run ./scripts/start-local.sh or ./scripts/start-multichain.sh first" + exit 1 fi # Initialize state file if not exists @@ -69,19 +113,30 @@ init() { "total_updates": 0, "total_deviations_detected": 0, "total_missed_updates": 0, + "receivers": ${#RECEIVERS[@]}, "symbols": {} } EOF log_info "Monitor initialized" - log_info "Receiver: $RECEIVER_ADDR" - log_info "DIA Oracle: ${DIA_ORACLE_ADDR:-N/A}" + log_info "Source Oracle: ${DIA_ORACLE_ADDR:-N/A} (Chain: ${SOURCE_CHAIN_ID:-N/A})" + log_info "Receivers discovered: ${#RECEIVERS[@]}" + for receiver in "${RECEIVERS[@]}"; do + IFS=':' read -r chain_id num addr <<< "$receiver" + log_info " Chain $chain_id Receiver $num: $addr" + done log_info "Deviation threshold: ${DEVIATION_THRESHOLD}%" log_info "Time threshold: ${TIME_THRESHOLD}s" log_info "Poll interval: ${POLL_INTERVAL}s" log_info "Log file: $LOG_FILE" } +get_rpc_url() { + local chain_id="$1" + local port=$(get_chain_port "$chain_id") + echo "http://localhost:$port" +} + ####################################### # Logging Functions ####################################### @@ -131,10 +186,13 @@ log_error() { ####################################### # Get price from PushOracleReceiverV2 (returns "value timestamp" as integers) +# Args: symbol rpc_url receiver_addr get_receiver_price() { local symbol="$1" + local rpc_url="$2" + local receiver_addr="$3" local result - result=$(cast call --rpc-url "$RPC_URL" "$RECEIVER_ADDR" \ + result=$(cast call --rpc-url "$rpc_url" "$receiver_addr" \ "getValue(string)(uint128,uint128)" "$symbol" 2>/dev/null || echo "0\n0") # Parse and normalize the output # Format is: "value [scientific]\ntimestamp [scientific]" @@ -167,14 +225,16 @@ print(f'{val} {ts}') } # Get price from DIAOracleV2 (source oracle) +# Uses source chain RPC get_source_price() { local symbol="$1" - if [ -z "$DIA_ORACLE_ADDR" ]; then + if [ -z "$DIA_ORACLE_ADDR" ] || [ -z "$SOURCE_CHAIN_ID" ]; then echo "0 0" return fi + local source_rpc=$(get_rpc_url "$SOURCE_CHAIN_ID") local result - result=$(cast call --rpc-url "$RPC_URL" "$DIA_ORACLE_ADDR" \ + result=$(cast call --rpc-url "$source_rpc" "$DIA_ORACLE_ADDR" \ "getValue(string)(uint128,uint128)" "$symbol" 2>/dev/null || echo "0\n0") # Parse and normalize the output python3 -c " @@ -244,10 +304,13 @@ except: # State Management ####################################### -# Get previous state for a symbol +# Get previous state for a receiver+symbol +# Args: chain_id receiver_num symbol get_state() { - local symbol="$1" - local key=$(echo "$symbol" | tr '/' '_') + local chain_id="$1" + local receiver_num="$2" + local symbol="$3" + local key="${chain_id}_${receiver_num}_$(echo "$symbol" | tr '/' '_')" python3 -c " import json try: @@ -260,15 +323,18 @@ except: " 2>/dev/null || echo "0 0 0 0 0" } -# Save state for a symbol +# Save state for a receiver+symbol +# Args: chain_id receiver_num symbol value timestamp source_value source_timestamp [last_deviation_time] save_state() { - local symbol="$1" - local value="$2" - local timestamp="$3" - local source_value="$4" - local source_timestamp="$5" - local last_deviation_time="${6:-0}" - local key=$(echo "$symbol" | tr '/' '_') + local chain_id="$1" + local receiver_num="$2" + local symbol="$3" + local value="$4" + local timestamp="$5" + local source_value="$6" + local source_timestamp="$7" + local last_deviation_time="${8:-0}" + local key="${chain_id}_${receiver_num}_$(echo "$symbol" | tr '/' '_')" python3 -c " import json @@ -349,12 +415,20 @@ with open('$METRICS_FILE', 'w') as f: # Monitoring Logic ####################################### -check_symbol() { - local symbol="$1" +# Check a single receiver for a symbol +# Args: chain_id receiver_num receiver_addr symbol +check_receiver_symbol() { + local chain_id="$1" + local receiver_num="$2" + local receiver_addr="$3" + local symbol="$4" local now=$(date +%s) + local rpc_url=$(get_rpc_url "$chain_id") + local short_addr="${receiver_addr:0:6}...${receiver_addr: -4}" + # Get current receiver price - local receiver_result=$(get_receiver_price "$symbol") + local receiver_result=$(get_receiver_price "$symbol" "$rpc_url" "$receiver_addr") local receiver_value=$(echo "$receiver_result" | awk '{print $1}') local receiver_timestamp=$(echo "$receiver_result" | awk '{print $2}') @@ -364,7 +438,7 @@ check_symbol() { local source_timestamp=$(echo "$source_result" | awk '{print $2}') # Get previous state - local prev_state=$(get_state "$symbol") + local prev_state=$(get_state "$chain_id" "$receiver_num" "$symbol") local prev_value=$(echo "$prev_state" | awk '{print $1}') local prev_timestamp=$(echo "$prev_state" | awk '{print $2}') local prev_source_value=$(echo "$prev_state" | awk '{print $3}') @@ -373,13 +447,12 @@ check_symbol() { # Convert to human readable local receiver_price=$(wei_to_price "$receiver_value") local source_price=$(wei_to_price "$source_value") - local prev_receiver_price=$(wei_to_price "$prev_value") - # Check for new update (use Python for safe comparison) + # Check for new update local is_new_update=$(python3 -c "print('yes' if '$receiver_timestamp' != '$prev_timestamp' and '$receiver_timestamp' != '0' else 'no')" 2>/dev/null || echo "no") if [ "$is_new_update" = "yes" ]; then - log_update "$symbol: New update received! Price: \$$receiver_price (ts: $receiver_timestamp)" - update_metrics "update" "$symbol" "$receiver_price" + log_update "[Chain:$chain_id] [${short_addr}] $symbol: New update received! Price: \$$receiver_price (ts: $receiver_timestamp)" + update_metrics "update" "${chain_id}_${receiver_num}_${symbol}" "$receiver_price" fi # Check deviation between source and receiver @@ -389,10 +462,10 @@ check_symbol() { local deviation_exceeded=$(python3 -c "print('yes' if float('$deviation') >= float('$DEVIATION_THRESHOLD') else 'no')" 2>/dev/null || echo "no") if [ "$deviation_exceeded" = "yes" ]; then - log_deviation "$symbol: Deviation detected! Source: \$$source_price, Receiver: \$$receiver_price, Deviation: ${deviation}%" - update_metrics "deviation" "$symbol" + log_deviation "[Chain:$chain_id] [${short_addr}] $symbol: Deviation detected! Source: \$$source_price, Receiver: \$$receiver_price, Deviation: ${deviation}%" + update_metrics "deviation" "${chain_id}_${receiver_num}_${symbol}" - # Check if update is overdue (use Python for arithmetic) + # Check if update is overdue local time_check=$(python3 -c " try: now = int('$now') @@ -409,13 +482,13 @@ except: if [[ "$time_check" == overdue* ]]; then local time_since=$(echo "$time_check" | awk '{print $2}') - log_missed "$symbol: UPDATE EXPECTED! Deviation: ${deviation}% > ${DEVIATION_THRESHOLD}%, Time since update: ${time_since}s > ${TIME_THRESHOLD}s" - update_metrics "missed" "$symbol" + log_missed "[Chain:$chain_id] [${short_addr}] $symbol: UPDATE EXPECTED! Deviation: ${deviation}% > ${DEVIATION_THRESHOLD}%, Time since update: ${time_since}s > ${TIME_THRESHOLD}s" + update_metrics "missed" "${chain_id}_${receiver_num}_${symbol}" fi fi fi - # Check for stale data (use Python for arithmetic) + # Check for stale data local stale_check=$(python3 -c " try: ts = int('$receiver_timestamp') @@ -435,47 +508,85 @@ except: if [[ "$stale_check" == stale* ]]; then local age=$(echo "$stale_check" | awk '{print $2}') - log_warning "$symbol: Data is stale! Last update: ${age}s ago" + log_warning "[Chain:$chain_id] [${short_addr}] $symbol: Data is stale! Last update: ${age}s ago" fi # Save current state - save_state "$symbol" "$receiver_value" "$receiver_timestamp" "$source_value" "$source_timestamp" "$now" + save_state "$chain_id" "$receiver_num" "$symbol" "$receiver_value" "$receiver_timestamp" "$source_value" "$source_timestamp" "$now" +} + +# Check all receivers for all symbols +check_all_receivers() { + for receiver in "${RECEIVERS[@]}"; do + IFS=':' read -r chain_id receiver_num receiver_addr <<< "$receiver" + for symbol in "${SYMBOLS[@]}"; do + check_receiver_symbol "$chain_id" "$receiver_num" "$receiver_addr" "$symbol" + done + done } print_status() { echo "" - echo "═══════════════════════════════════════════════════════════════" - echo " PushOracleReceiverV2 Monitor Status - $(timestamp)" - echo "═══════════════════════════════════════════════════════════════" + echo "══════════════════════════════════════════════════════════════════════════════════════" + echo " Multi-Chain PushOracleReceiverV2 Monitor - $(timestamp)" + echo " Source Oracle: ${DIA_ORACLE_ADDR:-N/A} (Chain: ${SOURCE_CHAIN_ID:-N/A})" + echo " Receivers: ${#RECEIVERS[@]} | Symbols: ${SYMBOLS[*]}" + echo "══════════════════════════════════════════════════════════════════════════════════════" - for symbol in "${SYMBOLS[@]}"; do - local receiver_result=$(get_receiver_price "$symbol") - local receiver_value=$(echo "$receiver_result" | awk '{print $1}') - local receiver_timestamp=$(echo "$receiver_result" | awk '{print $2}') - local receiver_price=$(wei_to_price "$receiver_value") - - local source_result=$(get_source_price "$symbol") - local source_value=$(echo "$source_result" | awk '{print $1}') - local source_price=$(wei_to_price "$source_value") - - local now=$(date +%s) + local now=$(date +%s) + + # Get and display source prices + echo "" + echo " Source Prices:" + local eth_source_result=$(get_source_price "ETH/USD") + local eth_source_value=$(echo "$eth_source_result" | awk '{print $1}') + local eth_source_price=$(wei_to_price "$eth_source_value") + echo " ETH/USD: \$$eth_source_price" + + local btc_source_result=$(get_source_price "BTC/USD") + local btc_source_value=$(echo "$btc_source_result" | awk '{print $1}') + local btc_source_price=$(wei_to_price "$btc_source_value") + echo " BTC/USD: \$$btc_source_price" + + echo "" + printf " %-8s | %-10s | %-42s | %-10s | %-12s | %-10s | %-8s\n" \ + "Chain" "Receiver" "Contract" "Symbol" "Price" "Deviation" "Age" + echo " ---------|------------|--------------------------------------------|-----------.|--------------|------------|--------" + + for receiver in "${RECEIVERS[@]}"; do + IFS=':' read -r chain_id receiver_num receiver_addr <<< "$receiver" + local rpc_url=$(get_rpc_url "$chain_id") - # Calculate age using Python - local age=$(python3 -c " + for symbol in "${SYMBOLS[@]}"; do + local receiver_result=$(get_receiver_price "$symbol" "$rpc_url" "$receiver_addr") + local receiver_value=$(echo "$receiver_result" | awk '{print $1}') + local receiver_timestamp=$(echo "$receiver_result" | awk '{print $2}') + local receiver_price=$(wei_to_price "$receiver_value") + + # Get source value for this symbol + local source_value + if [ "$symbol" = "ETH/USD" ]; then + source_value="$eth_source_value" + else + source_value="$btc_source_value" + fi + + # Calculate age + local age=$(python3 -c " try: ts = int('$receiver_timestamp') if ts == 0: - print('N/A (no data)') + print('N/A') else: now = int('$now') age = now - ts - print(f'{age}s ago') + print(f'{age}s') except: print('N/A') " 2>/dev/null || echo "N/A") - - # Calculate deviation - local deviation=$(python3 -c " + + # Calculate deviation + local deviation=$(python3 -c " try: src = int('$source_value') rcv = int('$receiver_value') @@ -483,21 +594,31 @@ try: print('N/A') else: dev = abs(src - rcv) / rcv * 100 - print(f'{dev:.4f}%') + print(f'{dev:.2f}%') except: print('N/A') " 2>/dev/null || echo "N/A") - - echo "" - echo " $symbol:" - echo " Receiver Price: \$$receiver_price" - echo " Source Price: \$$source_price" - echo " Deviation: $deviation" - echo " Last Update: $age" + + # Color code based on status + local status_color="$NC" + if [[ "$deviation" != "N/A" ]]; then + local dev_num=$(echo "$deviation" | tr -d '%') + local is_high=$(python3 -c "print('yes' if float('$dev_num') >= float('$DEVIATION_THRESHOLD') else 'no')" 2>/dev/null || echo "no") + if [ "$is_high" = "yes" ]; then + status_color="$YELLOW" + fi + fi + if [[ "$age" == "N/A" ]] || [[ "$receiver_price" == "0.00" ]]; then + status_color="$RED" + fi + + printf " ${status_color}%-8s${NC} | ${status_color}%-10s${NC} | ${status_color}%-42s${NC} | ${status_color}%-10s${NC} | ${status_color}\$%-11s${NC} | ${status_color}%-10s${NC} | ${status_color}%-8s${NC}\n" \ + "$chain_id" "#$receiver_num" "$receiver_addr" "$symbol" "$receiver_price" "$deviation" "$age" + done done echo "" - echo "═══════════════════════════════════════════════════════════════" + echo "══════════════════════════════════════════════════════════════════════════════════════" echo "" } @@ -512,15 +633,15 @@ monitor_loop() { print_status while true; do - for symbol in "${SYMBOLS[@]}"; do - check_symbol "$symbol" - done + check_all_receivers sleep "$POLL_INTERVAL" done } show_help() { - echo "PushOracleReceiverV2 Monitor" + echo "Multi-Chain PushOracleReceiverV2 Monitor" + echo "" + echo "Monitors ALL chains and ALL receivers from the multichain deployment." echo "" echo "Usage: $0 [command]" echo "" @@ -532,7 +653,6 @@ show_help() { echo " help Show this help" echo "" echo "Environment Variables:" - echo " RPC_URL RPC endpoint (default: http://localhost:8545)" echo " POLL_INTERVAL Seconds between checks (default: 5)" echo " DEVIATION_THRESHOLD Deviation % to trigger alert (default: 0.5)" echo " TIME_THRESHOLD Seconds before update expected (default: 120)" diff --git a/scripts/start-multichain.sh b/scripts/start-multichain.sh new file mode 100755 index 0000000..7bb3a4b --- /dev/null +++ b/scripts/start-multichain.sh @@ -0,0 +1,972 @@ +#!/usr/bin/env bash +set -euo pipefail + +####################################### +# Multi-Chain Local Development Setup +# Starts 10 chains with 12 PushOracleReceiverV2 contracts +####################################### + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Global variables +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CONTRACTS_DIR="${ROOT_DIR}/contracts" +COMPOSE_FILE="${ROOT_DIR}/docker-compose.local.yml" + +# Multi-chain configuration +# Format: CHAIN_ID:PORT:NAME:NUM_RECEIVERS +# 10 chains, 12 receivers total (some chains have 2 receivers) +CHAINS=( + "31337:8545:anvil-main:2" # Main chain with 2 receivers + "31338:8546:anvil-eth:1" # ETH L2 + "31339:8547:anvil-arb:2" # Arbitrum-like with 2 receivers + "31340:8548:anvil-opt:1" # Optimism-like + "31341:8549:anvil-base:1" # Base-like + "31342:8550:anvil-poly:1" # Polygon-like + "31343:8551:anvil-avax:1" # Avalanche-like + "31344:8552:anvil-bsc:1" # BSC-like + "31345:8553:anvil-ftm:1" # Fantom-like + "31346:8554:anvil-zksync:1" # zkSync-like +) + +DEFAULT_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + +# Postgres config +POSTGRES_HOST="localhost" +POSTGRES_PORT="5432" +POSTGRES_USER="bridge" +POSTGRES_PASSWORD="password" +POSTGRES_DB="oracle_bridge" + +# Local stack directories +LOCAL_STACK_DIR="${ROOT_DIR}/.local-stack" +CONTRACTS_ADDR_DIR="${LOCAL_STACK_DIR}/contracts" +MULTICHAIN_DIR="${LOCAL_STACK_DIR}/multichain" +CONFIG_DIR="${LOCAL_STACK_DIR}/config" +WALLETS_DIR="${LOCAL_STACK_DIR}/wallets" +PIDS_DIR="${LOCAL_STACK_DIR}/pids" + +# Track started processes +declare -a ANVIL_PIDS=() +SERVICES_STARTED=false + +####################################### +# Logging Functions +####################################### + +timestamp() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +log_info() { + echo -e "${BLUE}[$(timestamp)]${NC} ${CYAN}[INFO]${NC} $1" +} + +log_success() { + echo -e "${BLUE}[$(timestamp)]${NC} ${GREEN}[OK]${NC} $1" +} + +log_warning() { + echo -e "${BLUE}[$(timestamp)]${NC} ${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${BLUE}[$(timestamp)]${NC} ${RED}[ERROR]${NC} $1" >&2 +} + +log_chain() { + local chain_id="$1" + local message="$2" + echo -e "${BLUE}[$(timestamp)]${NC} ${CYAN}[Chain $chain_id]${NC} $message" +} + +####################################### +# Cleanup +####################################### + +cleanup() { + local exit_code=$? + + if [ "$SERVICES_STARTED" = "true" ]; then + log_info "Cleaning up..." + + # Stop all Anvil processes + for pid in "${ANVIL_PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + done + + # Kill any remaining anvil processes + pkill -f "anvil.*854" 2>/dev/null || true + + # Stop Docker services + docker compose -f "${COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true + + # Stop price updater + if [ -f "${PIDS_DIR}/price-updater.pid" ]; then + kill $(cat "${PIDS_DIR}/price-updater.pid") 2>/dev/null || true + fi + + log_success "Cleanup complete" + fi + exit $exit_code +} + +trap cleanup EXIT INT TERM + +####################################### +# Dependency Checks +####################################### + +check_dependencies() { + log_info "Checking dependencies..." + local missing=() + + command -v docker &>/dev/null || missing+=("docker") + command -v anvil &>/dev/null || missing+=("anvil") + command -v cast &>/dev/null || missing+=("cast") + command -v forge &>/dev/null || missing+=("forge") + command -v python3 &>/dev/null || missing+=("python3") + + if [ ${#missing[@]} -gt 0 ]; then + log_error "Missing dependencies: ${missing[*]}" + exit 1 + fi + + if ! docker info &>/dev/null; then + log_error "Docker is not running" + exit 1 + fi + + log_success "All dependencies satisfied" +} + +####################################### +# Initialize Directories +####################################### + +init_directories() { + log_info "Initializing directories..." + + mkdir -p "$LOCAL_STACK_DIR" + mkdir -p "$CONTRACTS_ADDR_DIR" + mkdir -p "$MULTICHAIN_DIR" + mkdir -p "$CONFIG_DIR" + mkdir -p "$WALLETS_DIR" + mkdir -p "$PIDS_DIR" + mkdir -p "${LOCAL_STACK_DIR}/logs" + + # Create chain-specific directories + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + mkdir -p "${MULTICHAIN_DIR}/${chain_id}" + done + + log_success "Directories initialized" +} + +####################################### +# Start Anvil Chains +####################################### + +start_anvil_chain() { + local chain_id="$1" + local port="$2" + local name="$3" + + log_chain "$chain_id" "Starting $name on port $port..." + + # Kill any existing process on this port + pkill -f "anvil.*--port $port" 2>/dev/null || true + sleep 1 + + # Start anvil with low base fee for local testing + anvil --host 0.0.0.0 --port "$port" --chain-id "$chain_id" --balance 10000 \ + --base-fee 1000000000 --silent & + local pid=$! + + # Verify anvil started + sleep 0.5 + if ! kill -0 "$pid" 2>/dev/null; then + log_error "Anvil process $pid died immediately" + return 1 + fi + + ANVIL_PIDS+=("$pid") + echo "$pid" > "${PIDS_DIR}/anvil-${chain_id}.pid" + + # Wait for chain to be ready with proper RPC check + local rpc="http://localhost:$port" + for i in {1..30}; do + local response + response=$(curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$rpc" 2>/dev/null) + if echo "$response" | grep -q "result"; then + log_chain "$chain_id" "Ready at $rpc (PID: $pid)" + return 0 + fi + sleep 0.5 + done + + log_error "Chain $chain_id failed to start" + return 1 +} + +start_all_chains() { + log_info "Starting ${#CHAINS[@]} Anvil chains..." + + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + start_anvil_chain "$chain_id" "$port" "$name" + done + + # Extra delay to ensure all chains are fully ready + sleep 2 + + log_success "All ${#CHAINS[@]} chains started" +} + +####################################### +# Deploy Contracts +####################################### + +deploy_contract() { + local rpc="$1" + local contract_path="$2" + shift 2 + local constructor_args=("$@") + + local output + local exit_code=0 + + if [ ${#constructor_args[@]} -gt 0 ]; then + output=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge create \ + --rpc-url "$rpc" \ + --private-key "$DEFAULT_KEY" \ + --broadcast \ + "$contract_path" \ + --constructor-args "${constructor_args[@]}" 2>&1) || exit_code=$? + else + output=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 forge create \ + --rpc-url "$rpc" \ + --private-key "$DEFAULT_KEY" \ + --broadcast \ + "$contract_path" 2>&1) || exit_code=$? + fi + + if [ $exit_code -ne 0 ]; then + log_error "Failed to deploy $contract_path" + log_error "$output" + echo "" + return 1 + fi + + local address=$(echo "$output" | grep -i "Deployed to:" | awk '{print $3}') + + if [ -z "$address" ]; then + log_error "Could not parse address from: $output" + echo "" + return 1 + fi + + echo "$address" +} + +# Verify chain is ready before deploying +verify_chain_ready() { + local rpc="$1" + local chain_id="$2" + local max_attempts=10 + + for i in $(seq 1 $max_attempts); do + if cast chain-id --rpc-url "$rpc" &>/dev/null; then + return 0 + fi + sleep 1 + done + + log_error "Chain $chain_id at $rpc is not responding" + return 1 +} + +# Deploy source chain contracts (main chain only - has Registry) +deploy_source_chain() { + local chain_id="31337" + local port="8545" + local rpc="http://localhost:$port" + local chain_dir="${MULTICHAIN_DIR}/${chain_id}" + + # Verify chain is ready + verify_chain_ready "$rpc" "$chain_id" || return 1 + + log_chain "$chain_id" "Deploying SOURCE chain contracts..." + + cd "$CONTRACTS_DIR" + + # Deploy DIAOracleV2 (source of price data) + local oracle_addr + oracle_addr=$(deploy_contract "$rpc" "contracts/DIAOracleV2.sol:DIAOracleV2") + if [ -z "$oracle_addr" ]; then + log_error "Failed to deploy DIAOracleV2" + return 1 + fi + echo "$oracle_addr" > "${chain_dir}/dia_oracle_v2.addr" + echo "$oracle_addr" > "${CONTRACTS_ADDR_DIR}/dia_oracle_v2.addr" + log_chain "$chain_id" "DIAOracleV2 (Source): $oracle_addr" + + # Deploy OracleIntentRegistry (ONLY on source chain) + # Constructor args: name, version + local registry_addr + registry_addr=$(deploy_contract "$rpc" \ + "contracts/OracleIntentRegistry.sol:OracleIntentRegistry" \ + "DIA Oracle" "1.0") + if [ -z "$registry_addr" ]; then + log_error "Failed to deploy OracleIntentRegistry" + return 1 + fi + echo "$registry_addr" > "${chain_dir}/oracle_intent_registry.addr" + echo "$registry_addr" > "${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr" + log_chain "$chain_id" "OracleIntentRegistry: $registry_addr" + + # Authorize signer in registry + local signer_addr + signer_addr=$(FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast wallet address --private-key "$DEFAULT_KEY") + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$registry_addr" "setSignerAuthorization(address,bool)" \ + "$signer_addr" true &>/dev/null || log_warning "Failed to authorize signer in registry" + + # Initialize oracle prices + local eth_price btc_price ts + eth_price=$(python3 -c "print(int(2250 * 1e18))") + btc_price=$(python3 -c "print(int(45000 * 1e18))") + ts=$(date +%s) + + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$oracle_addr" "setValue(string,uint128,uint128)" "ETH/USD" "$eth_price" "$ts" &>/dev/null || true + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$oracle_addr" "setValue(string,uint128,uint128)" "BTC/USD" "$btc_price" "$ts" &>/dev/null || true + + log_chain "$chain_id" "Source chain configured" +} + +# Deploy destination chain contracts (ProtocolFeeHook + Receivers) +deploy_chain_contracts() { + local chain_id="$1" + local port="$2" + local num_receivers="$3" + local rpc="http://localhost:$port" + local chain_dir="${MULTICHAIN_DIR}/${chain_id}" + + # Verify chain is ready + verify_chain_ready "$rpc" "$chain_id" || return 1 + + # Get the source chain registry address (for EIP-712 domain) + local source_registry=$(cat "${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr" 2>/dev/null) + if [ -z "$source_registry" ]; then + log_error "Source registry not found. Deploy source chain first." + return 1 + fi + + log_chain "$chain_id" "Deploying destination contracts (${num_receivers} receiver(s))..." + + cd "$CONTRACTS_DIR" + + # Deploy ProtocolFeeHook (required on EVERY chain for receivers) + local fee_hook_addr=$(deploy_contract "$rpc" "contracts/ProtocolFeeHook.sol:ProtocolFeeHook") + echo "$fee_hook_addr" > "${chain_dir}/protocol_fee_hook.addr" + log_chain "$chain_id" "ProtocolFeeHook: $fee_hook_addr" + + # Deploy PushOracleReceiverV2 (multiple if needed) + # Constructor args: domainName, domainVersion, sourceChainId, verifyingContract (registry on source) + # Source chain ID is always 31337 (main chain) + for i in $(seq 1 "$num_receivers"); do + local receiver_addr + receiver_addr=$(deploy_contract "$rpc" \ + "contracts/PushOracleReceiverV2.sol:PushOracleReceiverV2" \ + "DIA Oracle" "1.0" "31337" "$source_registry") + echo "$receiver_addr" > "${chain_dir}/push_oracle_receiver_v2_${i}.addr" + log_chain "$chain_id" "Receiver #$i: $receiver_addr" + + # Configure receiver - set payment hook + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$receiver_addr" "setPaymentHook(address)" "$fee_hook_addr" &>/dev/null || true + + # Authorize signer in receiver + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$receiver_addr" "setSignerAuthorization(address,bool)" \ + "$(cast wallet address --private-key $DEFAULT_KEY)" true &>/dev/null || true + + # Fund receiver with ETH for gas + FOUNDRY_DISABLE_NIGHTLY_WARNING=1 cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + --value "100ether" "$receiver_addr" &>/dev/null || true + done + + log_chain "$chain_id" "Destination chain configured" +} + +deploy_all_contracts() { + log_info "Deploying contracts to all chains..." + + # Step 1: Deploy source chain first (has OracleIntentRegistry) + deploy_source_chain + + # Step 2: Deploy destination contracts on ALL chains (including main chain receivers) + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + deploy_chain_contracts "$chain_id" "$port" "$num_receivers" + done + + log_success "All contracts deployed: 1 Registry (source), ${#CHAINS[@]} ProtocolFeeHooks, 12 Receivers" +} + +####################################### +# Generate Bridge Configuration +####################################### + +generate_bridge_config() { + log_info "Generating bridge configuration..." + + local bridge_config_dir="${CONFIG_DIR}/bridge-modular" + local routers_dir="${bridge_config_dir}/routers" + + # Clean up old config files to avoid stale router references + log_info "Cleaning old bridge config..." + rm -rf "${bridge_config_dir}" + mkdir -p "$routers_dir" + + # Generate infrastructure.yaml + cat > "${bridge_config_dir}/infrastructure.yaml" << 'EOF' +database: + driver: postgres + dsn_env: DATABASE_DSN +source: + chain_id: 31337 + name: Anvil Main + rpc_urls: + - env:SOURCE_RPC_URL + ws_url: ws://host.docker.internal:8545 + start_block: 0 +private_key_env: PRIVATE_KEY +event_monitor: + enabled: true + reconnectinterval: 5s + maxreconnectattempts: 10 +block_scanner: + enabled: true + scaninterval: 10s + blockrange: 100 + maxblockgap: 1000 + backwardsync: true +event_processor: + batchsize: 10 + validationtimeout: 30s + dedupcachesize: 1000 + dedupcachettl: 1h + enableparallelmode: false +worker_pool: + maxworkers: 10 + taskqueuesize: 200 + tasktimeout: 2m + retrydelay: 10s + maxretries: 3 +health_check: + enabled: true + checkinterval: 30s + timeout: 10s + maxprocessinglag: 2m + maxqueuesize: 50 +api: + enabled: true + listenaddr: :8080 + enablecors: true +metrics: + enabled: true + namespace: oracle_bridge +dry_run: false +EOF + + # Generate chains.yaml with all chains + cat > "${bridge_config_dir}/chains.yaml" << EOF +chains: +EOF + + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + cat >> "${bridge_config_dir}/chains.yaml" << EOF + "${chain_id}": + chain_id: ${chain_id} + name: ${name} + rpc_urls: + - http://host.docker.internal:${port} + enabled: true + default_gas_limit: 300000 + gas_multiplier: 1.2 + max_gas_price: "100000000000" +EOF + done + + # Generate contracts.yaml with all receivers + cat > "${bridge_config_dir}/contracts.yaml" << EOF +contracts: +EOF + + local contract_idx=1 + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + local chain_dir="${MULTICHAIN_DIR}/${chain_id}" + + for i in $(seq 1 "$num_receivers"); do + local receiver_addr=$(cat "${chain_dir}/push_oracle_receiver_v2_${i}.addr" 2>/dev/null || echo "0x0") + cat >> "${bridge_config_dir}/contracts.yaml" << EOF + push_oracle_receiver_${chain_id}_${i}: + chain_id: ${chain_id} + address: ${receiver_addr} + type: pushoracle + enabled: true + abi: '[{"name":"handleIntentUpdate","type":"function","inputs":[{"name":"intent","type":"tuple","components":[{"name":"intentType","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"expiry","type":"uint256"},{"name":"symbol","type":"string"},{"name":"price","type":"uint256"},{"name":"timestamp","type":"uint256"},{"name":"source","type":"string"},{"name":"signature","type":"bytes"},{"name":"signer","type":"address"}]}]}]' + gas_limit: 300000 + methods: + intent_update: + methodname: handleIntentUpdate + fieldsmapping: + intent: fullIntent + gaslimit: 300000 +EOF + contract_idx=$((contract_idx + 1)) + done + done + + # Generate events.yaml (registry is only on source chain 31337) + local source_registry=$(cat "${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr" 2>/dev/null || echo "0x0") + cat > "${bridge_config_dir}/events.yaml" << EOF +event_definitions: + IntentRegistered: + contract: ${source_registry} + abi: '{"name":"IntentRegistered","type":"event","inputs":[{"name":"intentHash","type":"bytes32","indexed":true},{"name":"symbol","type":"string","indexed":true},{"name":"price","type":"uint256","indexed":true},{"name":"timestamp","type":"uint256","indexed":false},{"name":"signer","type":"address","indexed":false}]}' + dataextraction: + intentHash: topics[1] + symbol: topics[2] + price: topics[3] + timestamp: timestamp + signer: signer + enrichment: + contract: "" + method: getIntent + abi: '{"name":"getIntent","type":"function","inputs":[{"name":"intentHash","type":"bytes32"}],"outputs":[{"name":"intent","type":"tuple","components":[{"name":"intentType","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"expiry","type":"uint256"},{"name":"symbol","type":"string"},{"name":"price","type":"uint256"},{"name":"timestamp","type":"uint256"},{"name":"source","type":"string"},{"name":"signature","type":"bytes"},{"name":"signer","type":"address"}]}]}' + params: + - \${event.intentHash} + returns: + fullIntent: "0" +EOF + + # Generate routers for each chain/receiver combination + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + + for i in $(seq 1 "$num_receivers"); do + cat > "${routers_dir}/router_${chain_id}_${i}.yaml" << EOF +router: + id: router_${chain_id}_${i} + name: router_chain_${chain_id}_receiver_${i} + type: event + enabled: true + private_key_env: PRIVATE_KEY + triggers: + events: + - IntentRegistered + conditions: [] + processing: + datasource: enrichment + transformations: [] + validationenabled: true + destinations: + - contract_ref: push_oracle_receiver_${chain_id}_${i} + time_threshold: 2s + price_deviation: "0.5%" + method: + name: handleIntentUpdate + abi: '{"name":"handleIntentUpdate","type":"function","inputs":[{"name":"intent","type":"tuple","components":[{"name":"intentType","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"nonce","type":"uint256"},{"name":"expiry","type":"uint256"},{"name":"symbol","type":"string"},{"name":"price","type":"uint256"},{"name":"timestamp","type":"uint256"},{"name":"source","type":"string"},{"name":"signature","type":"bytes"},{"name":"signer","type":"address"}]}]}' + params: + intent: \${enrichment.fullIntent} + value: "0" + gaslimit: 300000 + gasmultiplier: 1.2 +EOF + done + done + + log_success "Bridge configuration generated with $(ls ${routers_dir}/*.yaml | wc -l | tr -d ' ') routers" +} + +####################################### +# Generate Attestor Configuration +####################################### + +generate_attestor_config() { + log_info "Generating attestor configuration..." + + local main_oracle=$(cat "${MULTICHAIN_DIR}/31337/dia_oracle_v2.addr" 2>/dev/null || echo "0x0") + local main_registry=$(cat "${MULTICHAIN_DIR}/31337/oracle_intent_registry.addr" 2>/dev/null || echo "0x0") + + cat > "${CONFIG_DIR}/attestor.env" << EOF +ATTESTOR_RPC_URLS=http://host.docker.internal:8545 +ATTESTOR_ORACLE_ADDRESS=${main_oracle} +ATTESTOR_ORACLE_CLIENT_TYPE=dia_v2 +ATTESTOR_REGISTRY_ADDRESS=${main_registry} +ATTESTOR_ATTESTOR_PRIVATE_KEY=${DEFAULT_KEY} +ATTESTOR_ATTESTOR_SYMBOLS=BTC/USD,ETH/USD +ATTESTOR_ATTESTOR_POLLING_TIME=5s +ATTESTOR_ATTESTOR_BATCH_MODE=false +ATTESTOR_ATTESTOR_MODE=prime +ATTESTOR_METRICS_PORT=8080 +ATTESTOR_API_PORT=8081 +EOF + + cat > "${CONFIG_DIR}/attestor-local.yaml" << EOF +rpc: + url: http://host.docker.internal:8545 + urls: + - http://host.docker.internal:8545 + registry_url: http://host.docker.internal:8545 + registry_urls: + - http://host.docker.internal:8545 + +oracle: + address: "${main_oracle}" + client_type: "dia_v2" + +registry: + address: "${main_registry}" + +attestor: + private_key: "${DEFAULT_KEY}" + symbols: + - BTC/USD + - ETH/USD + polling_time: 5s + batch_mode: false + mode: prime + +logging: + level: debug + +metrics: + port: 8080 + +api: + port: 8081 +EOF + + log_success "Attestor configuration generated" +} + +####################################### +# Start Docker Services (Bridge, Attestor, Postgres) +####################################### + +start_docker_services() { + log_info "Starting Docker services (Postgres, Attestor, Bridge)..." + + # Export environment variables for docker-compose + export INTENT_REGISTRY_ADDRESS=$(cat "${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr" 2>/dev/null || echo "") + export PRIVATE_KEY="${DEFAULT_KEY}" + export POSTGRES_HOST="${POSTGRES_HOST}" + export POSTGRES_PORT="${POSTGRES_PORT}" + export POSTGRES_USER="${POSTGRES_USER}" + export POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" + export POSTGRES_DB="${POSTGRES_DB}" + # Use 'postgres' hostname for Docker container networking + export POSTGRES_DSN="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:${POSTGRES_PORT}/${POSTGRES_DB}?sslmode=disable" + + # Build and start services + log_info "Building Docker images..." + if ! docker compose -f "${COMPOSE_FILE}" build attestor bridge 2>&1 | tail -5; then + log_warning "Docker build had issues, continuing..." + fi + + log_info "Starting Postgres, Attestor, and Bridge..." + if ! docker compose -f "${COMPOSE_FILE}" up -d postgres attestor bridge; then + log_error "Failed to start Docker services" + return 1 + fi + + log_success "Docker services started" +} + +wait_for_services() { + log_info "Waiting for services to be healthy..." + + # Wait for Postgres + log_info "Waiting for Postgres..." + for i in {1..30}; do + if docker exec spectra-interoperability-postgres-1 pg_isready -U bridge &>/dev/null; then + log_success "Postgres is ready" + break + fi + sleep 1 + done + + # Wait for Attestor + log_info "Waiting for Attestor..." + for i in {1..60}; do + if curl -s http://localhost:8081/health &>/dev/null; then + log_success "Attestor is ready" + break + fi + if ! docker ps --filter "name=spectra-interoperability-attestor-1" --filter "status=running" | grep -q attestor; then + log_warning "Attestor container not running" + docker logs spectra-interoperability-attestor-1 --tail 10 2>/dev/null || true + break + fi + sleep 2 + done + + # Wait for Bridge + log_info "Waiting for Bridge..." + for i in {1..60}; do + if curl -s http://localhost:8082/metrics &>/dev/null; then + log_success "Bridge is ready" + break + fi + if ! docker ps --filter "name=spectra-interoperability-bridge-1" --filter "status=running" | grep -q bridge; then + log_warning "Bridge container not running" + docker logs spectra-interoperability-bridge-1 --tail 10 2>/dev/null || true + break + fi + sleep 2 + done + + log_success "Services health check complete" +} + +####################################### +# Start Price Updater +####################################### + +start_price_updater() { + log_info "Starting price updater for all chains..." + + # Create multi-chain price updater + cat > "${LOCAL_STACK_DIR}/price-updater-multichain.sh" << 'SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +MULTICHAIN_DIR="$1" +DEFAULT_KEY="$2" + +update_price() { + local rpc="$1" + local oracle="$2" + local symbol="$3" + local price="$4" + + local price_wei=$(python3 -c "print(int($price * 1e18))") + local ts=$(date +%s) + + cast send --rpc-url "$rpc" --private-key "$DEFAULT_KEY" \ + "$oracle" "setValue(string,uint128,uint128)" "$symbol" "$price_wei" "$ts" \ + 2>/dev/null || true +} + +while true; do + eth_price=$(python3 -c "import random; print(f'{2250 * (1 + random.uniform(-0.05, 0.05)):.2f}')") + btc_price=$(python3 -c "import random; print(f'{45000 * (1 + random.uniform(-0.03, 0.03)):.2f}')") + + for chain_dir in "$MULTICHAIN_DIR"/*/; do + chain_id=$(basename "$chain_dir") + oracle_file="${chain_dir}/dia_oracle_v2.addr" + + if [ -f "$oracle_file" ]; then + oracle=$(cat "$oracle_file") + # Determine port from chain_id + port=$((8545 + chain_id - 31337)) + rpc="http://localhost:$port" + + update_price "$rpc" "$oracle" "ETH/USD" "$eth_price" + update_price "$rpc" "$oracle" "BTC/USD" "$btc_price" + fi + done + + echo "[$(date -u +%H:%M:%S)] Updated prices: ETH/USD=$eth_price, BTC/USD=$btc_price" + sleep 10 +done +SCRIPT + chmod +x "${LOCAL_STACK_DIR}/price-updater-multichain.sh" + + nohup "${LOCAL_STACK_DIR}/price-updater-multichain.sh" "$MULTICHAIN_DIR" "$DEFAULT_KEY" \ + > "${LOCAL_STACK_DIR}/logs/price-updater.log" 2>&1 & + echo $! > "${PIDS_DIR}/price-updater.pid" + + log_success "Price updater started (PID: $(cat ${PIDS_DIR}/price-updater.pid))" +} + +####################################### +# Show Summary +####################################### + +show_summary() { + echo "" + echo "═══════════════════════════════════════════════════════════════════════════" + echo " Multi-Chain Local Development Environment" + echo "═══════════════════════════════════════════════════════════════════════════" + echo "" + echo " Architecture:" + echo " Source Chain: 31337 (anvil-main)" + echo " - DIAOracleV2 (price source)" + echo " - OracleIntentRegistry (intent registration & EIP-712 domain)" + echo "" + echo " Destination Chains: ${#CHAINS[@]} chains" + echo " - ProtocolFeeHook (1 per chain)" + echo " - PushOracleReceiverV2 (12 total across all chains)" + echo "" + echo " Source Chain (31337):" + echo " Oracle: $(cat ${CONTRACTS_ADDR_DIR}/dia_oracle_v2.addr 2>/dev/null || echo 'N/A')" + echo " Registry: $(cat ${CONTRACTS_ADDR_DIR}/oracle_intent_registry.addr 2>/dev/null || echo 'N/A')" + echo "" + echo " Destination Chains:" + + local total_receivers=0 + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + local chain_dir="${MULTICHAIN_DIR}/${chain_id}" + + echo "" + echo " ┌─ Chain $chain_id ($name) - http://localhost:$port" + echo " │ FeeHook: $(cat ${chain_dir}/protocol_fee_hook.addr 2>/dev/null || echo 'N/A')" + + for i in $(seq 1 "$num_receivers"); do + local receiver=$(cat "${chain_dir}/push_oracle_receiver_v2_${i}.addr" 2>/dev/null || echo 'N/A') + echo " │ Receiver #$i: $receiver" + total_receivers=$((total_receivers + 1)) + done + echo " └─" + done + + echo "" + echo " Total: ${#CHAINS[@]} chains, ${#CHAINS[@]} FeeHooks, $total_receivers Receivers" + echo "" + echo " Docker Services:" + echo " Postgres: localhost:5432" + echo " Attestor: localhost:8080 (metrics), localhost:8081 (API)" + echo " Bridge: localhost:8082 (metrics)" + echo "" + echo " Configuration:" + echo " Bridge: ${CONFIG_DIR}/bridge-modular/" + echo " Attestor: ${CONFIG_DIR}/attestor-local.yaml" + echo "" + echo " Logs:" + echo " Attestor: docker logs -f spectra-interoperability-attestor-1" + echo " Bridge: docker logs -f spectra-interoperability-bridge-1" + echo " Prices: tail -f ${LOCAL_STACK_DIR}/logs/price-updater.log" + echo "" + echo " Commands:" + echo " Monitor receivers: ./scripts/monitor-multichain.sh" + echo " Service logs: docker compose -f docker-compose.local.yml logs -f" + echo " Stop all: ./scripts/start-multichain.sh stop" + echo "" + echo "═══════════════════════════════════════════════════════════════════════════" +} + +####################################### +# Stop All +####################################### + +stop_all() { + log_info "Stopping all services..." + + # Stop Anvil processes + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + if [ -f "${PIDS_DIR}/anvil-${chain_id}.pid" ]; then + kill $(cat "${PIDS_DIR}/anvil-${chain_id}.pid") 2>/dev/null || true + rm -f "${PIDS_DIR}/anvil-${chain_id}.pid" + fi + done + + # Kill any remaining anvil + pkill -f "anvil.*854" 2>/dev/null || true + + # Stop price updater + if [ -f "${PIDS_DIR}/price-updater.pid" ]; then + kill $(cat "${PIDS_DIR}/price-updater.pid") 2>/dev/null || true + rm -f "${PIDS_DIR}/price-updater.pid" + fi + + # Stop Docker + docker compose -f "${COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true + + log_success "All services stopped" +} + +####################################### +# Show Status +####################################### + +show_status() { + echo "" + echo "Chain Status:" + echo "" + + for chain_config in "${CHAINS[@]}"; do + IFS=':' read -r chain_id port name num_receivers <<< "$chain_config" + local rpc="http://localhost:$port" + + if curl -s -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + "$rpc" &>/dev/null; then + echo -e " ${GREEN}●${NC} Chain $chain_id ($name): Running at $rpc" + else + echo -e " ${RED}●${NC} Chain $chain_id ($name): Not running" + fi + done + echo "" +} + +####################################### +# Main +####################################### + +main() { + local command="${1:-start}" + + case "$command" in + start) + log_info "Starting Multi-Chain Local Environment..." + SERVICES_STARTED=true + + check_dependencies + init_directories + start_all_chains + deploy_all_contracts + generate_bridge_config + generate_attestor_config + start_docker_services + wait_for_services + start_price_updater + show_summary + + log_info "Press Ctrl+C to stop all chains and services" + + # Wait for first anvil to keep script running + wait "${ANVIL_PIDS[0]}" + ;; + stop) + stop_all + ;; + status) + show_status + ;; + *) + echo "Usage: $0 [start|stop|status]" + exit 1 + ;; + esac +} + +main "$@" From 5998e29b6016c9db58b893dee3dd15789874e225 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Sun, 1 Feb 2026 17:38:06 +0530 Subject: [PATCH 5/8] chore: add more logs --- services/bridge/internal/bridge/bridge.go | 15 ++++++ services/bridge/internal/bridge/health.go | 13 +++++ .../internal/bridge/transaction_handler.go | 18 +++++-- .../processor/generic_event_processor.go | 21 ++++---- .../bridge/internal/transaction/executor.go | 2 +- .../bridge/internal/worker/worker_pool.go | 54 +++++++++++++++++-- 6 files changed, 105 insertions(+), 18 deletions(-) diff --git a/services/bridge/internal/bridge/bridge.go b/services/bridge/internal/bridge/bridge.go index 96d320a..39dad19 100644 --- a/services/bridge/internal/bridge/bridge.go +++ b/services/bridge/internal/bridge/bridge.go @@ -538,6 +538,20 @@ func (b *Bridge) processUpdates(ctx context.Context) { case <-b.shutdownChan: return case updateReq := <-b.eventSource.GetUpdateChan(): + // Log that we received an update from the queue + symbol := "unknown" + if updateReq.Intent != nil && updateReq.Intent.Symbol != "" { + symbol = updateReq.Intent.Symbol + } else if updateReq.Event != nil { + symbol = updateReq.Event.EventName + } + chainID := int64(0) + if updateReq.DestinationChain != nil { + chainID = updateReq.DestinationChain.ChainID + } + logger.Infof("[UPDATE-DEQUEUE] Received update from queue: router=%s, symbol=%s, chain=%d, queue_remaining=%d", + updateReq.RouterID, symbol, chainID, b.eventSource.GetQueueSize()) + // Report metric: when we successfully dequeue, we know there was at least 1 item // Report current size + 1 to show the size BEFORE we dequeued this item // This gives a more accurate picture of queue depth @@ -613,6 +627,7 @@ func (b *Bridge) processUpdates(ctx context.Context) { taskID = fmt.Sprintf("Process Updates unknown-%d-%d", updateReq.DestinationChain.ChainID, time.Now().Unix()) } + logger.Infof("[UPDATE-SUBMIT] Submitting to worker pool: task=%s, router=%s", taskID, updateReq.RouterID) b.workerPool.Submit(&worker.WorkerTask{ ID: taskID, Request: updateReq, diff --git a/services/bridge/internal/bridge/health.go b/services/bridge/internal/bridge/health.go index 670b177..dece6b0 100644 --- a/services/bridge/internal/bridge/health.go +++ b/services/bridge/internal/bridge/health.go @@ -62,6 +62,19 @@ func (b *Bridge) healthCheck(ctx context.Context) { // performHealthCheck performs health checks on all chains func (b *Bridge) performHealthCheck(ctx context.Context) { + // Log worker pool status for debugging + if b.workerPool != nil { + workerStats := b.workerPool.GetStats() + queueSize := 0 + if b.eventSource != nil { + queueSize = b.eventSource.GetQueueSize() + } + logger.Infof("[HEALTH] Worker pool: active=%d/%d, pending=%d/%d, update_queue=%d", + workerStats.ActiveTasks, workerStats.MaxWorkers, + workerStats.PendingTasks, workerStats.TotalCapacity, + queueSize) + } + // Check source chain sourceConfig := b.configService.GetInfrastructure().Source if err := b.checkChainHealth(ctx, b.readClient, sourceConfig.ChainID); err != nil { diff --git a/services/bridge/internal/bridge/transaction_handler.go b/services/bridge/internal/bridge/transaction_handler.go index f00c4a7..27da6fb 100644 --- a/services/bridge/internal/bridge/transaction_handler.go +++ b/services/bridge/internal/bridge/transaction_handler.go @@ -52,12 +52,17 @@ func NewTransactionHandler(writeClients map[int64]*WriteClient, registry *router // Process handles the complete transaction lifecycle func (h *TransactionHandler) Process(ctx context.Context, updateReq *bridgetypes.UpdateRequest) error { + startTime := time.Now() + logger.Infof("[TX-HANDLER] Starting transaction processing: router=%s, chain=%d, contract=%s", + updateReq.RouterID, updateReq.DestinationChain.ChainID, updateReq.Contract.Address) + txCtx, err := h.buildContext(ctx, updateReq) if err != nil { + logger.Errorf("[TX-HANDLER] Failed to build context: router=%s, error=%v", updateReq.RouterID, err) return err } - logger.Infof("Processing update for %s on chain %d", txCtx.Identifier, txCtx.UpdateRequest.DestinationChain.ChainID) + logger.Infof("Processing update for %s on chain %d (elapsed=%v)", txCtx.Identifier, txCtx.UpdateRequest.DestinationChain.ChainID, time.Since(startTime)) if err := h.validate(txCtx); err != nil { return err @@ -157,9 +162,16 @@ func (h *TransactionHandler) executeWithMethodConfig(txCtx *TransactionContext) func (h *TransactionHandler) confirm(txCtx *TransactionContext, tx *types.Transaction) error { h.recordSubmission(txCtx, tx.Hash().Hex()) + logger.Infof("[TX-CONFIRM] Waiting for receipt: tx=%s, router=%s, symbol=%s, chain=%d", + tx.Hash().Hex(), txCtx.UpdateRequest.RouterID, txCtx.Symbol, txCtx.UpdateRequest.DestinationChain.ChainID) + + confirmStartTime := time.Now() receipt, err := h.waitForReceipt(txCtx.Ctx, txCtx.DestClient.client, tx.Hash()) if err != nil { h.recordFailure(txCtx, "confirmation", "receipt_timeout") + logger.Errorf("[TX-CONFIRM] Failed to get receipt after %v: tx=%s, router=%s, symbol=%s, chain=%d, error=%v", + time.Since(confirmStartTime), tx.Hash().Hex(), txCtx.UpdateRequest.RouterID, txCtx.Symbol, + txCtx.UpdateRequest.DestinationChain.ChainID, err) return fmt.Errorf("failed to get transaction receipt: %w", err) } @@ -173,8 +185,8 @@ func (h *TransactionHandler) confirm(txCtx *TransactionContext, tx *types.Transa h.recordConfirmation(txCtx, tx.Hash().Hex(), receipt.GasUsed) h.updateState(txCtx) - logger.Infof("Transaction confirmed: %s, status: %d, gas used: %d, router=%s, symbol=%s", - tx.Hash().Hex(), receipt.Status, receipt.GasUsed, txCtx.UpdateRequest.RouterID, txCtx.Symbol) + logger.Infof("Transaction confirmed: %s, status: %d, gas used: %d, router=%s, symbol=%s, confirm_time=%v", + tx.Hash().Hex(), receipt.Status, receipt.GasUsed, txCtx.UpdateRequest.RouterID, txCtx.Symbol, time.Since(confirmStartTime)) return nil } diff --git a/services/bridge/internal/processor/generic_event_processor.go b/services/bridge/internal/processor/generic_event_processor.go index 45b85aa..dc3ec20 100644 --- a/services/bridge/internal/processor/generic_event_processor.go +++ b/services/bridge/internal/processor/generic_event_processor.go @@ -378,16 +378,17 @@ func (gep *GenericEventProcessor) processEvent(ctx context.Context, event *types } } - select { - case gep.updateChan <- updateReq: - routersUsed++ - symbol := router.GetSymbolFromData(extractedData) - logger.Infof("Queued update: event=%s, router=%s, symbol=%s, chain=%d, contract=%s", - event.EventName, result.RouterID, symbol, dest.ChainID, dest.Contract) - // Report queue size immediately after enqueueing - if gep.reportQueueSize != nil { - gep.reportQueueSize() - } + select { + case gep.updateChan <- updateReq: + routersUsed++ + atomic.AddUint64(&gep.stats.UpdatesCreated, 1) + symbol := router.GetSymbolFromData(extractedData) + logger.Infof("Queued update: event=%s, router=%s, symbol=%s, chain=%d, contract=%s, queue_size=%d/%d", + event.EventName, result.RouterID, symbol, dest.ChainID, dest.Contract, len(gep.updateChan), cap(gep.updateChan)) + // Report queue size immediately after enqueueing + if gep.reportQueueSize != nil { + gep.reportQueueSize() + } case <-ctx.Done(): return ctx.Err() default: diff --git a/services/bridge/internal/transaction/executor.go b/services/bridge/internal/transaction/executor.go index 44fffa6..e39d132 100644 --- a/services/bridge/internal/transaction/executor.go +++ b/services/bridge/internal/transaction/executor.go @@ -125,7 +125,7 @@ func (e *Executor) Execute(ctx context.Context, req *Request) (*types.Transactio // Refresh auth to get the newly allocated nonce auth = e.receiverClient.GetAuth() auth.GasLimit = req.GasLimit - auth.GasPrice = req.GasPrice + auth.Context = ctx // Get the nonce that was allocated diff --git a/services/bridge/internal/worker/worker_pool.go b/services/bridge/internal/worker/worker_pool.go index f426d32..fc0879c 100644 --- a/services/bridge/internal/worker/worker_pool.go +++ b/services/bridge/internal/worker/worker_pool.go @@ -236,6 +236,24 @@ func (w *Worker) start(ctx context.Context) { } } +// WorkerPoolStats contains worker pool statistics +type WorkerPoolStats struct { + ActiveTasks int32 + PendingTasks int + MaxWorkers int + TotalCapacity int +} + +// GetStats returns current worker pool statistics +func (wp *WorkerPool) GetStats() WorkerPoolStats { + return WorkerPoolStats{ + ActiveTasks: atomic.LoadInt32(&wp.activeWorkers), + PendingTasks: len(wp.taskQueue), + MaxWorkers: wp.maxWorkers, + TotalCapacity: cap(wp.taskQueue), + } +} + // processTask processes a single task func (w *Worker) processTask(ctx context.Context, task *WorkerTask) { // Track active workers @@ -244,7 +262,26 @@ func (w *Worker) processTask(ctx context.Context, task *WorkerTask) { startTime := time.Now() - logger.Debugf("Worker %d processing task %s", w.id, task.ID) + // Use Info level for task start to ensure visibility in production + symbol := "unknown" + chainID := int64(0) + routerID := "unknown" + if task.Request != nil { + routerID = task.Request.RouterID + if task.Request.Intent != nil && task.Request.Intent.Symbol != "" { + symbol = task.Request.Intent.Symbol + } + if task.Request.DestinationChain != nil { + chainID = task.Request.DestinationChain.ChainID + } + } + logger.Infof("[WORKER-%d] Starting task: %s, router=%s, symbol=%s, chain=%d, active_workers=%d", + w.id, task.ID, routerID, symbol, chainID, atomic.LoadInt32(&w.pool.activeWorkers)) + + // Create timeout context to prevent workers from blocking forever + // 6 minutes = enough time for receipt wait (5 min) + RPC calls + taskCtx, cancel := context.WithTimeout(ctx, 6*time.Minute) + defer cancel() // Process the task with retry logic var err error @@ -257,24 +294,33 @@ func (w *Worker) processTask(ctx context.Context, task *WorkerTask) { time.Sleep(time.Second * time.Duration(retry)) } - err = task.Handler(ctx, task) + err = task.Handler(taskCtx, task) if err == nil { break } + // Don't retry on context timeout/cancellation + if taskCtx.Err() != nil { + logger.Warnf("[WORKER-%d] Task %s context expired, not retrying: %v", w.id, task.ID, taskCtx.Err()) + err = taskCtx.Err() + break + } + logger.Errorf("Worker %d task %s failed (attempt %d/%d): %v", w.id, task.ID, retry+1, maxRetries, err) } duration := time.Since(startTime) if err != nil { - logger.Errorf("Worker %d task %s failed after %d retries: %v", w.id, task.ID, maxRetries, err) + logger.Errorf("[WORKER-%d] Task FAILED after %d retries: %s, router=%s, symbol=%s, chain=%d, duration=%v, error=%v", + w.id, maxRetries, task.ID, routerID, symbol, chainID, duration, err) if w.metricsCollector != nil { w.metricsCollector.IncWorkerTasksFailed() w.metricsCollector.ObserveTaskProcessingDuration(duration.Seconds()) } } else { - logger.Debugf("Worker %d completed task %s in %v (retries: %d)", w.id, task.ID, duration, retryCount) + logger.Infof("[WORKER-%d] Task COMPLETED: %s, router=%s, symbol=%s, chain=%d, duration=%v, retries=%d", + w.id, task.ID, routerID, symbol, chainID, duration, retryCount) if w.metricsCollector != nil { w.metricsCollector.IncWorkerTasksCompleted() w.metricsCollector.ObserveTaskProcessingDuration(duration.Seconds()) From b033a81e5047ceb99223d9f274fb979d059bb76b Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 6 Feb 2026 18:28:46 +0530 Subject: [PATCH 6/8] feat: take guarded config from nev key --- services/attestor/config.yaml.example | 13 +++- services/attestor/pkg/config/config.go | 94 ++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/services/attestor/config.yaml.example b/services/attestor/config.yaml.example index b7deaf9..fbdb8cd 100644 --- a/services/attestor/config.yaml.example +++ b/services/attestor/config.yaml.example @@ -8,6 +8,7 @@ rpc: # Oracle Configuration oracle: address: "0x0087342f5f4c7AB23a37c045c3EF710749527c88" + client_type: "guarded" # Options: "guarded" (uses getGuardedValue) or "dia_v2" (uses getValue) # Registry Configuration registry: @@ -39,6 +40,7 @@ api: # - ATTESTOR_RPC_URL or ATTESTOR_RPC_URLS (comma-separated for multiple URLs) # - ATTESTOR_RPC_REGISTRY_URL or ATTESTOR_RPC_REGISTRY_URLS (comma-separated for multiple URLs) # - ATTESTOR_ORACLE_ADDRESS +# - ATTESTOR_ORACLE_CLIENT_TYPE (guarded or dia_v2) # - ATTESTOR_REGISTRY_ADDRESS # - ATTESTOR_ATTESTOR_PRIVATE_KEY # - ATTESTOR_ATTESTOR_SYMBOLS (comma-separated) @@ -48,8 +50,17 @@ api: # - ATTESTOR_LOGGING_LEVEL # - ATTESTOR_METRICS_PORT # - ATTESTOR_API_PORT +# - ATTESTOR_GUARDIAN_ASSETS_CONFIG (JSON format for per-asset guardian config) # # Environment variables use dot notation converted to underscores: # - rpc.url -> ATTESTOR_RPC_URL # - attestor.private_key -> ATTESTOR_ATTESTOR_PRIVATE_KEY -# - etc. \ No newline at end of file +# - etc. +# +# Per-Asset Guardian Configuration via Environment Variable: +# You can also configure guardian parameters per asset using ATTESTOR_GUARDIAN_ASSETS_CONFIG: +# ATTESTOR_GUARDIAN_ASSETS_CONFIG='{ +# "BTC/USD": {"max_deviation_bips": 100, "max_timestamp_age": 1800}, +# "DEX:BTC/USD": {"max_deviation_bips": 150, "min_guardian_matches": 2} +# }' +# This JSON config takes precedence over YAML config for specific symbols. diff --git a/services/attestor/pkg/config/config.go b/services/attestor/pkg/config/config.go index 7b22519..6ec8ed4 100644 --- a/services/attestor/pkg/config/config.go +++ b/services/attestor/pkg/config/config.go @@ -1,10 +1,12 @@ package config import ( + "encoding/json" "fmt" "strings" "time" + "github.com/diadata.org/Spectra-interoperability/services/attestor/pkg/logger" "github.com/spf13/viper" ) @@ -20,6 +22,65 @@ func parseCSV(input string) []string { return result } +// parseAssetGuardianFromJSON parses per-asset guardian config from viper +// Expected JSON format (set via ATTESTOR_GUARDIAN_ASSETS_CONFIG env var): +// +// { +// "BTC/USD": { +// "max_deviation_bips": 100, +// "max_timestamp_age": 1800, +// "min_guardian_matches": 1 +// }, +// "DEX:BTC/USD": { +// "max_deviation_bips": 150, +// "min_guardian_matches": 2 +// } +// } +func parseAssetGuardianFromJSON(v *viper.Viper) (map[string]GuardianParams, error) { + // Get the JSON string from viper (bound to ATTESTOR_GUARDIAN_ASSETS_CONFIG) + jsonConfig := v.GetString("attestor.guardian.assets_config") + + if jsonConfig == "" { + return nil, nil + } + + jsonConfig = strings.TrimSpace(jsonConfig) + if jsonConfig == "" { + return nil, nil + } + + var rawConfig map[string]json.RawMessage + if err := json.Unmarshal([]byte(jsonConfig), &rawConfig); err != nil { + return nil, fmt.Errorf("failed to parse ATTESTOR_GUARDIAN_ASSETS_CONFIG JSON: %w\n", err) + } + + if len(rawConfig) == 0 { + return nil, nil + } + + assetConfig := make(map[string]GuardianParams) + + // Parse each asset's configuration + for symbol, rawParams := range rawConfig { + // Validate symbol is not empty + symbol = strings.TrimSpace(symbol) + if symbol == "" { + return nil, fmt.Errorf("invalid ATTESTOR_GUARDIAN_ASSETS_CONFIG: empty symbol key found") + } + + var params GuardianParams + if err := json.Unmarshal(rawParams, ¶ms); err != nil { + return nil, fmt.Errorf("failed to parse guardian parameters for symbol %q: %w\n"+ + "Please ensure the parameters are valid. Expected format:\n"+ + `{"max_deviation_bips": int, "max_timestamp_age": int, "min_guardian_matches": int}`, symbol, err) + } + + assetConfig[symbol] = params + } + + return assetConfig, nil +} + type Config struct { RPC struct { URL string `mapstructure:"url"` @@ -29,7 +90,7 @@ type Config struct { } `mapstructure:"rpc"` Oracle struct { - Address string `mapstructure:"address"` + Address string `mapstructure:"address"` ClientType OracleClientType `mapstructure:"client_type"` } `mapstructure:"oracle"` @@ -68,9 +129,9 @@ type GuardianConfig struct { } type GuardianParams struct { - MaxDeviationBips int `mapstructure:"max_deviation_bips"` - MaxTimestampAge int `mapstructure:"max_timestamp_age"` - MinGuardianMatches int `mapstructure:"min_guardian_matches"` + MaxDeviationBips int `mapstructure:"max_deviation_bips" json:"max_deviation_bips"` + MaxTimestampAge int `mapstructure:"max_timestamp_age" json:"max_timestamp_age"` + MinGuardianMatches int `mapstructure:"min_guardian_matches" json:"min_guardian_matches"` } // GetParamsForSymbol returns guardian parameters for a specific symbol. @@ -124,6 +185,7 @@ func Init(configPath string) (*Config, error) { v.BindEnv("attestor.guardian.default.max_deviation_bips", "ATTESTOR_GUARDIAN_MAX_DEVIATION_BIPS") v.BindEnv("attestor.guardian.default.max_timestamp_age", "ATTESTOR_GUARDIAN_MAX_TIMESTAMP_AGE") v.BindEnv("attestor.guardian.default.min_guardian_matches", "ATTESTOR_GUARDIAN_MIN_GUARDIAN_MATCHES") + v.BindEnv("attestor.guardian.assets_config", "ATTESTOR_GUARDIAN_ASSETS_CONFIG") // Set defaults v.SetDefault("rpc.url", "https://testnet-rpc.diadata.org") @@ -173,6 +235,30 @@ func Init(configPath string) (*Config, error) { cfg.Oracle.ClientType = clientType } + assetGuardianFromJSON, err := parseAssetGuardianFromJSON(v) + if err != nil { + return nil, fmt.Errorf("failed to parse ATTESTOR_GUARDIAN_ASSETS_CONFIG: %w", err) + } + + if len(assetGuardianFromJSON) > 0 { + if cfg.Attestor.Guardian.Symbols == nil { + cfg.Attestor.Guardian.Symbols = make(map[string]GuardianParams) + } + + logger.Info("Loading per-asset guardian configuration from ATTESTOR_GUARDIAN_ASSETS_CONFIG") + + for symbol, params := range assetGuardianFromJSON { + cfg.Attestor.Guardian.Symbols[symbol] = params + + logger.WithFields(map[string]interface{}{ + "symbol": symbol, + "max_deviation_bips": params.MaxDeviationBips, + "max_timestamp_age": params.MaxTimestampAge, + "min_guardian_matches": params.MinGuardianMatches, + }).Info("Loaded per-asset guardian configuration") + } + } + // Normalize RPC URLs configuration: convert single URL to array if needed if len(cfg.RPC.URLs) == 0 { if cfg.RPC.URL != "" { From 0b74e81416731fc0dfa155ff0d7d4d29f7f4b465 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 6 Feb 2026 18:30:30 +0530 Subject: [PATCH 7/8] chore: run gofmt --- .../attestor/pkg/client/guardianClient.go | 24 +++++++++---------- services/attestor/pkg/config/types.go | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/services/attestor/pkg/client/guardianClient.go b/services/attestor/pkg/client/guardianClient.go index 6d9f8cc..0615607 100644 --- a/services/attestor/pkg/client/guardianClient.go +++ b/services/attestor/pkg/client/guardianClient.go @@ -374,12 +374,12 @@ func (oc *GuardedOracleClient) GetGuardedValue(ctx context.Context, symbol strin numMinGuardianMatches := big.NewInt(int64(params.MinGuardianMatches)) logger.WithFields(map[string]interface{}{ - "symbol": symbol, - "oracle_address": oc.oracleAddr.Hex(), - "client_type": "guarded", - "contract_function": "getGuardedValue(string,uint256,uint256,uint256)", - "maxDeviationBips": params.MaxDeviationBips, - "maxTimestampAge": params.MaxTimestampAge, + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "client_type": "guarded", + "contract_function": "getGuardedValue(string,uint256,uint256,uint256)", + "maxDeviationBips": params.MaxDeviationBips, + "maxTimestampAge": params.MaxTimestampAge, "minGuardianMatches": params.MinGuardianMatches, }).Info("GuardedOracleClient: Calling getGuardedValue with guardian validation") @@ -410,11 +410,11 @@ func (oc *GuardedOracleClient) GetValue(ctx context.Context, symbol string) (*bi func (oc *GuardedOracleClient) fetchOracleValue(ctx context.Context, symbol string, maxDeviationBips, maxTimestampAge, numMinGuardianMatches *big.Int) (*big.Int, *big.Int, error) { logger.WithFields(map[string]interface{}{ - "symbol": symbol, - "oracle_address": oc.oracleAddr.Hex(), - "function": "getGuardedValue(string,uint256,uint256,uint256)", + "symbol": symbol, + "oracle_address": oc.oracleAddr.Hex(), + "function": "getGuardedValue(string,uint256,uint256,uint256)", }).Debug("Packing contract call for GuardedOracle.getGuardedValue") - + data, err := oc.oracleABI.Pack("getGuardedValue", symbol, maxDeviationBips, maxTimestampAge, numMinGuardianMatches) if err != nil { return nil, nil, fmt.Errorf("failed to pack input data for getGuardedValue: %v", err) @@ -426,10 +426,10 @@ func (oc *GuardedOracleClient) fetchOracleValue(ctx context.Context, symbol stri "oracle_address": oc.oracleAddr.Hex(), "function": "getGuardedValue(string,uint256,uint256,uint256)", }).Info("Calling GuardedOracle contract: getGuardedValue") - + resultBytes, err := oc.multiClient.CallContract(ctx, callMsg, nil) if err != nil { - return nil, nil, fmt.Errorf("contract call failed for getGuardedValue(%s, %d, %d, %d): %v", + return nil, nil, fmt.Errorf("contract call failed for getGuardedValue(%s, %d, %d, %d): %v", symbol, maxDeviationBips.Int64(), maxTimestampAge.Int64(), numMinGuardianMatches.Int64(), err) } diff --git a/services/attestor/pkg/config/types.go b/services/attestor/pkg/config/types.go index 49b1aa0..a8f1c98 100644 --- a/services/attestor/pkg/config/types.go +++ b/services/attestor/pkg/config/types.go @@ -69,7 +69,7 @@ type AttestorConfig struct { // OracleConfig holds oracle configuration type OracleConfig struct { - Address string `mapstructure:"address"` + Address string `mapstructure:"address"` ClientType OracleClientType `mapstructure:"client_type"` } From 7532f2ec2a4aac383d4f4ce5d2704eb95e8f9019 Mon Sep 17 00:00:00 2001 From: nnn-gif Date: Fri, 6 Feb 2026 18:34:27 +0530 Subject: [PATCH 8/8] chore: fix formatting issues in bridge service files --- services/bridge/internal/bridge/health.go | 4 ++-- .../processor/generic_event_processor.go | 22 +++++++++---------- .../bridge/pkg/router/generic_router_test.go | 8 +++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/services/bridge/internal/bridge/health.go b/services/bridge/internal/bridge/health.go index dece6b0..47e845d 100644 --- a/services/bridge/internal/bridge/health.go +++ b/services/bridge/internal/bridge/health.go @@ -17,13 +17,13 @@ func (b *Bridge) initializeChainStats() { logger.Warnf("configService is nil, skipping chain stats initialization") return } - + infra := b.configService.GetInfrastructure() if infra == nil { logger.Warnf("Infrastructure config is nil, skipping chain stats initialization") return } - + // Source chain stats sourceConfig := infra.Source b.stats.ChainStats[sourceConfig.ChainID] = &bridgetypes.ChainStatus{ diff --git a/services/bridge/internal/processor/generic_event_processor.go b/services/bridge/internal/processor/generic_event_processor.go index dc3ec20..30ac862 100644 --- a/services/bridge/internal/processor/generic_event_processor.go +++ b/services/bridge/internal/processor/generic_event_processor.go @@ -378,17 +378,17 @@ func (gep *GenericEventProcessor) processEvent(ctx context.Context, event *types } } - select { - case gep.updateChan <- updateReq: - routersUsed++ - atomic.AddUint64(&gep.stats.UpdatesCreated, 1) - symbol := router.GetSymbolFromData(extractedData) - logger.Infof("Queued update: event=%s, router=%s, symbol=%s, chain=%d, contract=%s, queue_size=%d/%d", - event.EventName, result.RouterID, symbol, dest.ChainID, dest.Contract, len(gep.updateChan), cap(gep.updateChan)) - // Report queue size immediately after enqueueing - if gep.reportQueueSize != nil { - gep.reportQueueSize() - } + select { + case gep.updateChan <- updateReq: + routersUsed++ + atomic.AddUint64(&gep.stats.UpdatesCreated, 1) + symbol := router.GetSymbolFromData(extractedData) + logger.Infof("Queued update: event=%s, router=%s, symbol=%s, chain=%d, contract=%s, queue_size=%d/%d", + event.EventName, result.RouterID, symbol, dest.ChainID, dest.Contract, len(gep.updateChan), cap(gep.updateChan)) + // Report queue size immediately after enqueueing + if gep.reportQueueSize != nil { + gep.reportQueueSize() + } case <-ctx.Done(): return ctx.Err() default: diff --git a/services/bridge/pkg/router/generic_router_test.go b/services/bridge/pkg/router/generic_router_test.go index 620cfc7..bc17dc6 100644 --- a/services/bridge/pkg/router/generic_router_test.go +++ b/services/bridge/pkg/router/generic_router_test.go @@ -65,8 +65,8 @@ func TestGenericRouter_ShouldRoute(t *testing.T) { func TestGetSymbolsFromConfig(t *testing.T) { tests := []struct { - name string - routerConfig *config.RouterConfig + name string + routerConfig *config.RouterConfig expectedSymbols []string }{ { @@ -273,8 +273,8 @@ func TestGetSymbolsFromConfig(t *testing.T) { expectedSymbols: []string{}, }, { - name: "nil router config", - routerConfig: nil, + name: "nil router config", + routerConfig: nil, expectedSymbols: []string{}, }, {