diff --git a/bridge/cmd/guardiand/bridge.go b/bridge/cmd/guardiand/bridge.go index 6ee754a2d..7fd01af34 100644 --- a/bridge/cmd/guardiand/bridge.go +++ b/bridge/cmd/guardiand/bridge.go @@ -320,8 +320,11 @@ func runBridge(cmd *cobra.Command, args []string) { logger.Fatal("failed to load guardian key", zap.Error(err)) } + guardianAddr := ethcrypto.PubkeyToAddress(gk.PublicKey).String() logger.Info("Loaded guardian key", zap.String( - "address", ethcrypto.PubkeyToAddress(gk.PublicKey).String())) + "address", guardianAddr)) + + p2p.DefaultRegistry.SetGuardianAddress(guardianAddr) // Node's main lifecycle context. rootCtx, rootCtxCancel = context.WithCancel(context.Background()) diff --git a/bridge/pkg/ethereum/watcher.go b/bridge/pkg/ethereum/watcher.go index a4d5b4147..2050b214a 100644 --- a/bridge/pkg/ethereum/watcher.go +++ b/bridge/pkg/ethereum/watcher.go @@ -3,6 +3,8 @@ package ethereum import ( "context" "fmt" + "github.com/certusone/wormhole/bridge/pkg/p2p" + gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1" "math/big" "sync" "time" @@ -89,6 +91,11 @@ func NewEthBridgeWatcher(url string, bridge eth_common.Address, minConfirmations } func (e *EthBridgeWatcher) Run(ctx context.Context) error { + // Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing) + p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDEthereum, &gossipv1.Heartbeat_Network{ + BridgeAddress: e.bridge.Hex(), + }) + timeout, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() c, err := ethclient.DialContext(timeout, e.url) @@ -244,6 +251,10 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error { logger.Info("processing new header", zap.Stringer("block", ev.Number)) currentEthHeight.Set(float64(ev.Number.Int64())) readiness.SetReady(common.ReadinessEthSyncing) + p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDEthereum, &gossipv1.Heartbeat_Network{ + Height: ev.Number.Int64(), + BridgeAddress: e.bridge.Hex(), + }) e.pendingLocksGuard.Lock() diff --git a/bridge/pkg/p2p/p2p.go b/bridge/pkg/p2p/p2p.go index d7ff4636c..2eeedb0c3 100644 --- a/bridge/pkg/p2p/p2p.go +++ b/bridge/pkg/p2p/p2p.go @@ -187,18 +187,27 @@ func Run(obsvC chan *gossipv1.SignedObservation, case <-ctx.Done(): return case <-tick.C: + DefaultRegistry.mu.Lock() + networks := make([]*gossipv1.Heartbeat_Network, 0, len(DefaultRegistry.networkStats)) + for _, v := range DefaultRegistry.networkStats { + networks = append(networks, v) + } + msg := gossipv1.GossipMessage{Message: &gossipv1.GossipMessage_Heartbeat{ Heartbeat: &gossipv1.Heartbeat{ - NodeName: nodeName, - Counter: ctr, - Timestamp: time.Now().UnixNano(), - Version: version.Version(), + NodeName: nodeName, + Counter: ctr, + Timestamp: time.Now().UnixNano(), + Networks: networks, + Version: version.Version(), + GuardianAddr: DefaultRegistry.guardianAddress, }}} b, err := proto.Marshal(&msg) if err != nil { panic(err) } + DefaultRegistry.mu.Unlock() err = th.Publish(ctx, b) if err != nil { diff --git a/bridge/pkg/p2p/registry.go b/bridge/pkg/p2p/registry.go new file mode 100644 index 000000000..cabdddd2f --- /dev/null +++ b/bridge/pkg/p2p/registry.go @@ -0,0 +1,46 @@ +package p2p + +import ( + gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1" + "github.com/certusone/wormhole/bridge/pkg/vaa" + "sync" +) + +// The p2p package implements a simple global metrics registry singleton for node status values transmitted on-chain. + +type registry struct { + mu sync.Mutex + + // Mapping of chain IDs to network status messages. + networkStats map[vaa.ChainID]*gossipv1.Heartbeat_Network + + // Value of Heartbeat.guardian_addr. + guardianAddress string +} + +func NewRegistry() *registry { + return ®istry{ + networkStats: map[vaa.ChainID]*gossipv1.Heartbeat_Network{}, + } +} + +var ( + DefaultRegistry = NewRegistry() +) + +// SetGuardianAddress stores the node's guardian address to broadcast in Heartbeat messages. +// This should be called once during startup, when the guardian key is loaded. +func (r *registry) SetGuardianAddress(addr string) { + r.mu.Lock() + r.guardianAddress = addr + r.mu.Unlock() +} + +// SetNetworkStats sets the current network status to be broadcast in Heartbeat messages. +// The "Id" field is automatically set to the specified chain ID. +func (r *registry) SetNetworkStats(chain vaa.ChainID, data *gossipv1.Heartbeat_Network) { + r.mu.Lock() + data.Id = uint32(chain) + r.networkStats[chain] = data + r.mu.Unlock() +} diff --git a/bridge/pkg/solana/client.go b/bridge/pkg/solana/client.go index 1b55c540c..8529ae58a 100644 --- a/bridge/pkg/solana/client.go +++ b/bridge/pkg/solana/client.go @@ -6,11 +6,14 @@ import ( "encoding/binary" "fmt" "github.com/certusone/wormhole/bridge/pkg/common" + "github.com/certusone/wormhole/bridge/pkg/p2p" + gossipv1 "github.com/certusone/wormhole/bridge/pkg/proto/gossip/v1" "github.com/certusone/wormhole/bridge/pkg/supervisor" "github.com/certusone/wormhole/bridge/pkg/vaa" "github.com/dfuse-io/solana-go" "github.com/dfuse-io/solana-go/rpc" eth_common "github.com/ethereum/go-ethereum/common" + "github.com/mr-tron/base58" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" "math/big" @@ -65,6 +68,12 @@ func NewSolanaWatcher(wsUrl, rpcUrl string, bridgeAddress solana.PublicKey, lock } func (s *SolanaWatcher) Run(ctx context.Context) error { + // Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing) + bridgeAddr := base58.Encode(s.bridge[:]) + p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSolana, &gossipv1.Heartbeat_Network{ + BridgeAddress: bridgeAddr, + }) + rpcClient := rpc.NewClient(s.rpcUrl) logger := supervisor.Logger(ctx) errC := make(chan error) @@ -91,6 +100,10 @@ func (s *SolanaWatcher) Run(ctx context.Context) error { return } currentSolanaHeight.Set(float64(slot)) + p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSolana, &gossipv1.Heartbeat_Network{ + Height: int64(slot), + BridgeAddress: bridgeAddr, + }) logger.Info("current Solana height", zap.Uint64("slot", uint64(slot))) diff --git a/proto/gossip/v1/gossip.proto b/proto/gossip/v1/gossip.proto index c3106e2e0..5f455740a 100644 --- a/proto/gossip/v1/gossip.proto +++ b/proto/gossip/v1/gossip.proto @@ -11,26 +11,41 @@ message GossipMessage { } } -// P2P gossip heartbeats for network introspection purposes. +// P2P gossip heartbeats for network introspection purposes. ALL FIELDS ARE UNTRUSTED. message Heartbeat { // The node's arbitrarily chosen, untrusted nodeName. string node_name = 1; // A monotonic counter that resets to zero on startup. int64 counter = 2; // UNIX wall time. - int64 timestamp = 3; // TODO: use this + int64 timestamp = 3; - // Consensus heights on connected networks - message Network {// TODO: use this + message Network { + // Canonical chain ID. uint32 id = 1; + // Consensus height of the node. int64 height = 2; + // Chain-specific human-readable representation of the bridge contract address. + string bridge_address = 3; + + // Fee payer account for this network, if present. Some networks like Ethereum do not use fee payer accounts. + message FeePayer { + // The account's on-chain balance. + int64 balance = 1; + // Chain-specific human-readable representation of the fee payer account's address. + string address = 2; + } + FeePayer fee_payer = 4; } repeated Network networks = 4; // Human-readable representation of the current bridge node release. string version = 5; - // TODO: include statement of gk public key? + // Human-readable representation of the guardian key's address. + string guardian_addr = 6; + + // TODO: include signed statement of gk public key? } // A SignedObservation is a signed statement by a given guardian node