66 "fmt"
77 "slices"
88 "sort"
9+ "strings"
910 "sync"
1011 "time"
1112
@@ -25,6 +26,7 @@ import (
2526 "github.com/certusone/wormhole/node/pkg/gwrelayer"
2627 gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
2728 "github.com/certusone/wormhole/node/pkg/supervisor"
29+ "github.com/certusone/wormhole/node/pkg/watchers"
2830 "github.com/wormhole-foundation/wormhole/sdk/vaa"
2931
3032 "github.com/prometheus/client_golang/prometheus"
@@ -210,6 +212,9 @@ type Processor struct {
210212
211213 // managerC is the channel used to send signed VAAs to the manager service (nil if manager service is disabled)
212214 managerC chan <- * vaa.VAA
215+
216+ // publicRpcLogged tracks which chains have already had a public RPC warning logged.
217+ publicRpcLogged map [vaa.ChainID ]struct {}
213218}
214219
215220// updateVaaEntry is used to queue up a VAA to be written to the database.
@@ -284,6 +289,101 @@ var (
284289// batchObsvPubChanSize specifies the size of the channel used to publish observation batches. Allow five seconds worth.
285290const batchObsvPubChanSize = p2p .MaxObservationBatchSize * 5
286291
292+ // publicRPCDenyList contains domains for known public RPC endpoints.
293+ // Guardians using these for watcher URLs are at risk of serving incorrect data,
294+ // which can lead to consensus issues when the guardian is part of the delegated guardian set.
295+ var publicRPCDenyList = map [string ]struct {}{
296+ "infura.io" : {},
297+ "alchemy.com" : {},
298+ "quiknode.pro" : {},
299+ "ankr.com" : {},
300+ "blastapi.io" : {},
301+ "publicnode.com" : {},
302+ "llamarpc.com" : {},
303+ "1rpc.io" : {},
304+ "nodereal.io" : {},
305+ "bnbchain.org" : {},
306+ "pocket.network" : {},
307+ "monad.xyz" : {},
308+ "monadinfra.com" : {},
309+ "hyperliquid.xyz" : {},
310+ "t.conduit.xyz" : {},
311+ }
312+
313+ func publicRPCDomain (host string ) (string , bool ) {
314+ host = strings .ToLower (strings .TrimSuffix (host , "." ))
315+ for domain := host ; domain != "" ; {
316+ if _ , exists := publicRPCDenyList [domain ]; exists {
317+ return domain , true
318+ }
319+
320+ _ , parentDomain , foundParentDomain := strings .Cut (domain , "." )
321+ if ! foundParentDomain {
322+ return "" , false
323+ }
324+ domain = parentDomain
325+ }
326+
327+ return "" , false
328+ }
329+
330+ // checkPublicRpcEndpoints checks all chains in the DGS for public RPC URLs.
331+ // It logs an error once per chain per run if a public RPC endpoint is detected.
332+ // This is a safety measure: delegating guardians that use public RPC endpoints
333+ // risk producing incorrect VAAs, which can cause consensus failures.
334+ func (p * Processor ) checkPublicRpcEndpoints () {
335+ if ! p .delegatedGuardiansEnabled {
336+ return
337+ }
338+ if p .dgc == nil {
339+ p .logger .Warn ("public RPC endpoint check skipped: delegated guardian config is nil" )
340+ return
341+ }
342+
343+ allChains := p .dgc .ReadAll ()
344+ if len (allChains ) == 0 {
345+ p .logger .Warn ("public RPC endpoint check skipped: delegated guardian config has no chains" )
346+ return
347+ }
348+
349+ for chainID , cfg := range allChains {
350+ // Only check chains where this guardian is a delegated guardian
351+ if _ , ok := cfg .KeyIndex (p .ourAddr ); ! ok {
352+ continue
353+ }
354+
355+ // Skip if we've already logged for this chain.
356+ if _ , logged := p .publicRpcLogged [chainID ]; logged {
357+ continue
358+ }
359+
360+ rpcURLs := watchers .RPCURLs (chainID )
361+ if len (rpcURLs ) == 0 {
362+ continue
363+ }
364+
365+ for _ , rpcURL := range rpcURLs {
366+ hostname := common .SafeURLForLogging (rpcURL )
367+ if hostname == "<invalid-url>" {
368+ continue
369+ }
370+
371+ if domain , ok := publicRPCDomain (hostname ); ok {
372+ p .logger .Error ("public RPC endpoint detected for delegated guardian chain" ,
373+ zap .Stringer ("chainID" , chainID ),
374+ zap .String ("rpcHost" , hostname ),
375+ zap .String ("matchedDomain" , domain ),
376+ )
377+ p .publicRpcLogged [chainID ] = struct {}{}
378+ }
379+
380+ if _ , logged := p .publicRpcLogged [chainID ]; logged {
381+ break
382+ }
383+ }
384+ }
385+ }
386+
287387func NewProcessor (
288388 ctx context.Context ,
289389 db * guardianDB.Database ,
@@ -345,6 +445,7 @@ func NewProcessor(
345445 dgc : dgc ,
346446 delegatedGuardiansEnabled : delegatedGuardiansEnabled ,
347447 managerC : managerC ,
448+ publicRpcLogged : make (map [vaa.ChainID ]struct {}),
348449 }
349450}
350451
@@ -509,6 +610,8 @@ func (p *Processor) Run(ctx context.Context) error {
509610
510611 if err := p .dgc .Set (chains ); err != nil {
511612 p .logger .Error ("delegate guardian config update failed" , zap .Error (err ))
613+ } else {
614+ p .checkPublicRpcEndpoints ()
512615 }
513616 dgConfig .mu .Unlock ()
514617 case k := <- p .msgC :
0 commit comments