node/pkg/ethereum: configurable chain ID and name

Fixes wormhole/issues#247

Change-Id: Ieb792b73970603283e4ffc4a8b9217c85964fb9f
This commit is contained in:
Leo 2021-07-28 14:33:42 +02:00 committed by Leopold Schabel
parent 2c56a916eb
commit 863e0e69ec
2 changed files with 97 additions and 56 deletions

View File

@ -374,7 +374,7 @@ func runBridge(cmd *cobra.Command, args []string) {
} }
if err := supervisor.Run(ctx, "ethwatch", if err := supervisor.Run(ctx, "ethwatch",
ethereum.NewEthBridgeWatcher(*ethRPC, ethContractAddr, lockC, setC).Run); err != nil { ethereum.NewEthBridgeWatcher(*ethRPC, ethContractAddr, "eth", vaa.ChainIDEthereum, true, lockC, setC).Run); err != nil {
return err return err
} }

View File

@ -30,45 +30,61 @@ var (
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "wormhole_eth_connection_errors_total", Name: "wormhole_eth_connection_errors_total",
Help: "Total number of Ethereum connection errors (either during initial connection or while watching)", Help: "Total number of Ethereum connection errors (either during initial connection or while watching)",
}, []string{"reason"}) }, []string{"eth_network", "reason"})
ethMessagesObserved = promauto.NewCounter( ethMessagesObserved = promauto.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "wormhole_eth_messages_observed_total", Name: "wormhole_eth_messages_observed_total",
Help: "Total number of Eth messages observed (pre-confirmation)", Help: "Total number of Eth messages observed (pre-confirmation)",
}) }, []string{"eth_network"})
ethMessagesConfirmed = promauto.NewCounter( ethMessagesConfirmed = promauto.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "wormhole_eth_messages_confirmed_total", Name: "wormhole_eth_messages_confirmed_total",
Help: "Total number of Eth messages verified (post-confirmation)", Help: "Total number of Eth messages verified (post-confirmation)",
}) }, []string{"eth_network"})
guardianSetChangesConfirmed = promauto.NewCounter( guardianSetChangesConfirmed = promauto.NewCounterVec(
prometheus.CounterOpts{ prometheus.CounterOpts{
Name: "wormhole_eth_guardian_set_changes_confirmed_total", Name: "wormhole_eth_guardian_set_changes_confirmed_total",
Help: "Total number of guardian set changes verified (we only see confirmed ones to begin with)", Help: "Total number of guardian set changes verified (we only see confirmed ones to begin with)",
}) }, []string{"eth_network"})
currentEthHeight = promauto.NewGauge( currentEthHeight = promauto.NewGaugeVec(
prometheus.GaugeOpts{ prometheus.GaugeOpts{
Name: "wormhole_eth_current_height", Name: "wormhole_eth_current_height",
Help: "Current Ethereum block height", Help: "Current Ethereum block height",
}) }, []string{"eth_network"})
queryLatency = promauto.NewHistogramVec( queryLatency = promauto.NewHistogramVec(
prometheus.HistogramOpts{ prometheus.HistogramOpts{
Name: "wormhole_eth_query_latency", Name: "wormhole_eth_query_latency",
Help: "Latency histogram for Ethereum calls (note that most interactions are streaming queries, NOT calls, and we cannot measure latency for those", Help: "Latency histogram for Ethereum calls (note that most interactions are streaming queries, NOT calls, and we cannot measure latency for those",
}, []string{"operation"}) }, []string{"eth_network", "operation"})
) )
type ( type (
EthBridgeWatcher struct { EthBridgeWatcher struct {
url string // Ethereum RPC url
url string
// Address of the Eth bridge contract
bridge eth_common.Address bridge eth_common.Address
// Human-readable name of the Eth network, for logging and monitoring.
networkName string
// VAA ChainID of the network we're connecting to.
chainID vaa.ChainID
// Whether to publish a message to setChan whenever the guardian set changes.
// We currently only fetch the guardian set from one primary chain, which should
// have this flag set to true, and false on all others.
//
// The current primary chain is Ethereum (a mostly arbitrary decision because it
// has the best API - we might want to switch the primary chain to Solana once
// the governance mechanism lives there),
emitGuardianSet bool
// Channel to send new messages to.
lockChan chan *common.MessagePublication
// Channel to send guardian set changes to.
setChan chan *common.GuardianSet
pendingLocks map[eth_common.Hash]*pendingLock pendingLocks map[eth_common.Hash]*pendingLock
pendingLocksGuard sync.Mutex pendingLocksGuard sync.Mutex
lockChan chan *common.MessagePublication
setChan chan *common.GuardianSet
} }
pendingLock struct { pendingLock struct {
@ -77,13 +93,30 @@ type (
} }
) )
func NewEthBridgeWatcher(url string, bridge eth_common.Address, lockEvents chan *common.MessagePublication, setEvents chan *common.GuardianSet) *EthBridgeWatcher { func NewEthBridgeWatcher(
return &EthBridgeWatcher{url: url, bridge: bridge, lockChan: lockEvents, setChan: setEvents, pendingLocks: map[eth_common.Hash]*pendingLock{}} url string,
bridge eth_common.Address,
networkName string,
chainID vaa.ChainID,
emitGuardianSet bool,
lockEvents chan *common.MessagePublication,
setEvents chan *common.GuardianSet) *EthBridgeWatcher {
return &EthBridgeWatcher{
url: url,
bridge: bridge,
networkName: networkName,
emitGuardianSet: emitGuardianSet,
chainID: chainID,
lockChan: lockEvents,
setChan: setEvents,
pendingLocks: map[eth_common.Hash]*pendingLock{}}
} }
func (e *EthBridgeWatcher) Run(ctx context.Context) error { func (e *EthBridgeWatcher) Run(ctx context.Context) error {
logger := supervisor.Logger(ctx)
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing) // Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDEthereum, &gossipv1.Heartbeat_Network{ p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
BridgeAddress: e.bridge.Hex(), BridgeAddress: e.bridge.Hex(),
}) })
@ -91,7 +124,7 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
defer cancel() defer cancel()
c, err := ethclient.DialContext(timeout, e.url) c, err := ethclient.DialContext(timeout, e.url)
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues("dial_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "dial_error").Inc()
return fmt.Errorf("dialing eth client failed: %w", err) return fmt.Errorf("dialing eth client failed: %w", err)
} }
@ -113,7 +146,7 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
messageC := make(chan *abi.AbiLogMessagePublished, 2) messageC := make(chan *abi.AbiLogMessagePublished, 2)
messageSub, err := f.WatchLogMessagePublished(&bind.WatchOpts{Context: timeout}, messageC, nil) messageSub, err := f.WatchLogMessagePublished(&bind.WatchOpts{Context: timeout}, messageC, nil)
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues("subscribe_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "subscribe_error").Inc()
return fmt.Errorf("failed to subscribe to message publication events: %w", err) return fmt.Errorf("failed to subscribe to message publication events: %w", err)
} }
@ -121,40 +154,42 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
guardianSetC := make(chan *abi.AbiGuardianSetAdded, 2) guardianSetC := make(chan *abi.AbiGuardianSetAdded, 2)
guardianSetEvent, err := f.WatchGuardianSetAdded(&bind.WatchOpts{Context: timeout}, guardianSetC, nil) guardianSetEvent, err := f.WatchGuardianSetAdded(&bind.WatchOpts{Context: timeout}, guardianSetC, nil)
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues("subscribe_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "subscribe_error").Inc()
return fmt.Errorf("failed to subscribe to guardian set events: %w", err) return fmt.Errorf("failed to subscribe to guardian set events: %w", err)
} }
errC := make(chan error) // Get initial validator set.
logger := supervisor.Logger(ctx)
// Get initial validator set from Ethereum. We could also fetch it from Solana,
// because both sets are synchronized, we simply made an arbitrary decision to use Ethereum.
timeout, cancel = context.WithTimeout(ctx, 15*time.Second) timeout, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel() defer cancel()
idx, gs, err := FetchCurrentGuardianSet(timeout, e.url, e.bridge) idx, gs, err := FetchCurrentGuardianSet(timeout, e.url, e.bridge)
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues("guardian_set_fetch_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "guardian_set_fetch_error").Inc()
return fmt.Errorf("failed requesting guardian set from Ethereum: %w", err) return fmt.Errorf("failed requesting guardian set from Ethereum: %w", err)
} }
logger.Info("initial guardian set fetched", zap.Any("value", gs), zap.Uint32("index", idx)) logger.Info("initial guardian set fetched",
e.setChan <- &common.GuardianSet{ zap.Any("value", gs), zap.Uint32("index", idx),
Keys: gs.Keys, zap.String("eth_network", e.networkName))
Index: idx,
if e.emitGuardianSet {
e.setChan <- &common.GuardianSet{
Keys: gs.Keys,
Index: idx,
}
} }
errC := make(chan error)
go func() { go func() {
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case e := <-messageSub.Err(): case err := <-messageSub.Err():
ethConnectionErrors.WithLabelValues("subscription_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "subscription_error").Inc()
errC <- fmt.Errorf("error while processing message publication subscription: %w", e) errC <- fmt.Errorf("error while processing message publication subscription: %w", err)
return return
case e := <-guardianSetEvent.Err(): case err := <-guardianSetEvent.Err():
ethConnectionErrors.WithLabelValues("subscription_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "subscription_error").Inc()
errC <- fmt.Errorf("error while processing guardian set subscription: %w", e) errC <- fmt.Errorf("error while processing guardian set subscription: %w", err)
return return
case ev := <-messageC: case ev := <-messageC:
// Request timestamp for block // Request timestamp for block
@ -162,10 +197,10 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
timeout, cancel = context.WithTimeout(ctx, 15*time.Second) timeout, cancel = context.WithTimeout(ctx, 15*time.Second)
b, err := c.BlockByNumber(timeout, big.NewInt(int64(ev.Raw.BlockNumber))) b, err := c.BlockByNumber(timeout, big.NewInt(int64(ev.Raw.BlockNumber)))
cancel() cancel()
queryLatency.WithLabelValues("block_by_number").Observe(time.Since(msm).Seconds()) queryLatency.WithLabelValues(e.networkName, "block_by_number").Observe(time.Since(msm).Seconds())
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues("block_by_number_error").Inc() ethConnectionErrors.WithLabelValues(e.networkName, "block_by_number_error").Inc()
errC <- fmt.Errorf("failed to request timestamp for block %d: %w", ev.Raw.BlockNumber, err) errC <- fmt.Errorf("failed to request timestamp for block %d: %w", ev.Raw.BlockNumber, err)
return return
} }
@ -175,16 +210,16 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
Timestamp: time.Unix(int64(b.Time()), 0), Timestamp: time.Unix(int64(b.Time()), 0),
Nonce: ev.Nonce, Nonce: ev.Nonce,
Sequence: ev.Sequence, Sequence: ev.Sequence,
EmitterChain: vaa.ChainIDEthereum, EmitterChain: e.chainID,
EmitterAddress: PadAddress(ev.Sender), EmitterAddress: PadAddress(ev.Sender),
Payload: ev.Payload, Payload: ev.Payload,
ConsistencyLevel: ev.ConsistencyLevel, ConsistencyLevel: ev.ConsistencyLevel,
} }
logger.Info("found new message publication transaction", zap.Stringer("tx", ev.Raw.TxHash), logger.Info("found new message publication transaction", zap.Stringer("tx", ev.Raw.TxHash),
zap.Uint64("block", ev.Raw.BlockNumber)) zap.Uint64("block", ev.Raw.BlockNumber), zap.String("eth_network", e.networkName))
ethMessagesObserved.Inc() ethMessagesObserved.WithLabelValues(e.networkName).Inc()
e.pendingLocksGuard.Lock() e.pendingLocksGuard.Lock()
e.pendingLocks[ev.Raw.TxHash] = &pendingLock{ e.pendingLocks[ev.Raw.TxHash] = &pendingLock{
@ -194,15 +229,15 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
e.pendingLocksGuard.Unlock() e.pendingLocksGuard.Unlock()
case ev := <-guardianSetC: case ev := <-guardianSetC:
logger.Info("guardian set has changed, fetching new value", logger.Info("guardian set has changed, fetching new value",
zap.Uint32("new_index", ev.Index)) zap.Uint32("new_index", ev.Index), zap.String("eth_network", e.networkName))
guardianSetChangesConfirmed.Inc() guardianSetChangesConfirmed.WithLabelValues(e.networkName).Inc()
msm := time.Now() msm := time.Now()
timeout, cancel = context.WithTimeout(ctx, 15*time.Second) timeout, cancel = context.WithTimeout(ctx, 15*time.Second)
gs, err := caller.GetGuardianSet(&bind.CallOpts{Context: timeout}, ev.Index) gs, err := caller.GetGuardianSet(&bind.CallOpts{Context: timeout}, ev.Index)
cancel() cancel()
queryLatency.WithLabelValues("get_guardian_set").Observe(time.Since(msm).Seconds()) queryLatency.WithLabelValues(e.networkName, "get_guardian_set").Observe(time.Since(msm).Seconds())
if err != nil { if err != nil {
// We failed to process the guardian set update and are now out of sync with the chain. // We failed to process the guardian set update and are now out of sync with the chain.
// Recover by crashing the runnable, which causes the guardian set to be re-fetched. // Recover by crashing the runnable, which causes the guardian set to be re-fetched.
@ -210,10 +245,15 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
return return
} }
logger.Info("new guardian set fetched", zap.Any("value", gs), zap.Uint32("index", ev.Index)) logger.Info("new guardian set fetched",
e.setChan <- &common.GuardianSet{ zap.Any("value", gs), zap.Uint32("index", ev.Index),
Keys: gs.Keys, zap.String("eth_network", e.networkName))
Index: ev.Index,
if e.emitGuardianSet {
e.setChan <- &common.GuardianSet{
Keys: gs.Keys,
Index: ev.Index,
}
} }
} }
} }
@ -236,10 +276,11 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
return return
case ev := <-headSink: case ev := <-headSink:
start := time.Now() start := time.Now()
logger.Info("processing new header", zap.Stringer("block", ev.Number)) logger.Info("processing new header", zap.Stringer("block", ev.Number),
currentEthHeight.Set(float64(ev.Number.Int64())) zap.String("eth_network", e.networkName))
currentEthHeight.WithLabelValues(e.networkName).Set(float64(ev.Number.Int64()))
readiness.SetReady(common.ReadinessEthSyncing) readiness.SetReady(common.ReadinessEthSyncing)
p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDEthereum, &gossipv1.Heartbeat_Network{ p2p.DefaultRegistry.SetNetworkStats(e.chainID, &gossipv1.Heartbeat_Network{
Height: ev.Number.Int64(), Height: ev.Number.Int64(),
BridgeAddress: e.bridge.Hex(), BridgeAddress: e.bridge.Hex(),
}) })
@ -252,7 +293,7 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
// Transaction was dropped and never picked up again // Transaction was dropped and never picked up again
if pLock.height+4*uint64(pLock.lock.ConsistencyLevel) <= blockNumberU { if pLock.height+4*uint64(pLock.lock.ConsistencyLevel) <= blockNumberU {
logger.Debug("observation timed out", zap.Stringer("tx", pLock.lock.TxHash), logger.Debug("observation timed out", zap.Stringer("tx", pLock.lock.TxHash),
zap.Stringer("block", ev.Number)) zap.Stringer("block", ev.Number), zap.String("eth_network", e.networkName))
delete(e.pendingLocks, hash) delete(e.pendingLocks, hash)
continue continue
} }
@ -260,16 +301,16 @@ func (e *EthBridgeWatcher) Run(ctx context.Context) error {
// Transaction is now ready // Transaction is now ready
if pLock.height+uint64(pLock.lock.ConsistencyLevel) <= ev.Number.Uint64() { if pLock.height+uint64(pLock.lock.ConsistencyLevel) <= ev.Number.Uint64() {
logger.Debug("observation confirmed", zap.Stringer("tx", pLock.lock.TxHash), logger.Debug("observation confirmed", zap.Stringer("tx", pLock.lock.TxHash),
zap.Stringer("block", ev.Number)) zap.Stringer("block", ev.Number), zap.String("eth_network", e.networkName))
delete(e.pendingLocks, hash) delete(e.pendingLocks, hash)
e.lockChan <- pLock.lock e.lockChan <- pLock.lock
ethMessagesConfirmed.Inc() ethMessagesConfirmed.WithLabelValues(e.networkName).Inc()
} }
} }
e.pendingLocksGuard.Unlock() e.pendingLocksGuard.Unlock()
logger.Info("processed new header", zap.Stringer("block", ev.Number), logger.Info("processed new header", zap.Stringer("block", ev.Number),
zap.Duration("took", time.Since(start))) zap.Duration("took", time.Since(start)), zap.String("eth_network", e.networkName))
} }
} }
}() }()