node: optimism watcher support (#1746)

This commit is contained in:
bruce-riley 2022-11-10 07:50:08 -06:00 committed by GitHub
parent 41059a5dd4
commit bc1a740cfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 0 deletions

View File

@ -148,6 +148,9 @@ var (
arbitrumRPC *string
arbitrumContract *string
optimismRPC *string
optimismContract *string
logLevel *string
unsafeDevMode *bool
@ -274,6 +277,9 @@ func init() {
arbitrumRPC = NodeCmd.Flags().String("arbitrumRPC", "", "Arbitrum RPC URL")
arbitrumContract = NodeCmd.Flags().String("arbitrumContract", "", "Arbitrum contract address")
optimismRPC = NodeCmd.Flags().String("optimismRPC", "", "Optimism RPC URL")
optimismContract = NodeCmd.Flags().String("optimismContract", "", "Optimism contract address")
logLevel = NodeCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)")
unsafeDevMode = NodeCmd.Flags().Bool("unsafeDevMode", false, "Launch node in unsafe, deterministic devnet mode")
@ -445,6 +451,9 @@ func runNode(cmd *cobra.Command, args []string) {
if *arbitrumContract == "" {
*arbitrumContract = devnet.GanacheWormholeContractAddress.Hex()
}
if *optimismContract == "" {
*optimismContract = devnet.GanacheWormholeContractAddress.Hex()
}
}
// Verify flags
@ -580,6 +589,9 @@ func runNode(cmd *cobra.Command, args []string) {
if *injectiveContract == "" {
logger.Fatal("Please specify --injectiveContract")
}
if (*optimismRPC == "") != (*optimismContract == "") {
logger.Fatal("Both --optimismContract and --optimismRPC must be set together or both unset")
}
} else {
if *neonRPC != "" && !*unsafeDevMode {
logger.Fatal("Please do not specify --neonRPC")
@ -596,6 +608,12 @@ func runNode(cmd *cobra.Command, args []string) {
if *injectiveContract != "" && !*unsafeDevMode {
logger.Fatal("Please do not specify --injectiveContract")
}
if *optimismRPC != "" && !*unsafeDevMode {
logger.Fatal("Please do not specify --optimismRPC")
}
if *optimismContract != "" && !*unsafeDevMode {
logger.Fatal("Please do not specify --optimismContract")
}
}
if *nodeName == "" {
logger.Fatal("Please specify --nodeName")
@ -707,6 +725,7 @@ func runNode(cmd *cobra.Command, args []string) {
moonbeamContractAddr := eth_common.HexToAddress(*moonbeamContract)
neonContractAddr := eth_common.HexToAddress(*neonContract)
arbitrumContractAddr := eth_common.HexToAddress(*arbitrumContract)
optimismContractAddr := eth_common.HexToAddress(*optimismContract)
solAddress, err := solana_types.PublicKeyFromBase58(*solanaContract)
if err != nil {
logger.Fatal("invalid Solana contract address", zap.Error(err))
@ -1138,6 +1157,18 @@ func runNode(cmd *cobra.Command, args []string) {
return err
}
}
if shouldStart(optimismRPC) {
if ethWatcher == nil {
log.Fatalf("if optimism is enabled then ethereum must also be enabled.")
}
logger.Info("Starting Optimism watcher")
readiness.RegisterComponent(common.ReadinessOptimismSyncing)
chainObsvReqC[vaa.ChainIDOptimism] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
if err := supervisor.Run(ctx, "optimismwatch",
evm.NewEthWatcher(*optimismRPC, optimismContractAddr, "optimism", common.ReadinessOptimismSyncing, vaa.ChainIDOptimism, lockC, nil, 1, chainObsvReqC[vaa.ChainIDOptimism], *unsafeDevMode, ethWatcher).Run); err != nil {
return err
}
}
if shouldStart(injectiveWS) {
logger.Info("Starting Injective watcher")
readiness.RegisterComponent(common.ReadinessInjectiveSyncing)

View File

@ -26,5 +26,6 @@ const (
ReadinessXplaSyncing readiness.Component = "xplaSyncing"
ReadinessPythNetSyncing readiness.Component = "pythnetSyncing"
ReadinessArbitrumSyncing readiness.Component = "arbitrumSyncing"
ReadinessOptimismSyncing readiness.Component = "optimismSyncing"
ReadinessWormchainSyncing readiness.Component = "wormchainSyncing"
)

View File

@ -0,0 +1,139 @@
package finalizers
import (
"context"
"fmt"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
"github.com/certusone/wormhole/node/pkg/watchers/evm/interfaces"
"go.uber.org/zap"
)
// OptimismFinalizer implements the finality check for Optimism.
// Optimism provides a special "rollup_getInfo" API call to determine the latest L2 (Optimism) block to be published on the L1 (Ethereum).
// This finalizer polls that API to determine if a block is finalized.
type FinalizerEntry struct {
l2Block uint64
l1Block uint64
}
type OptimismFinalizer struct {
logger *zap.Logger
connector connectors.Connector
l1Finalizer interfaces.L1Finalizer
latestFinalizedL2Block uint64
// finalizerMapping is a array of FinalizerEntry structs with the L2 block number that has been verified and its corresponding L1 block number
finalizerMapping []FinalizerEntry
}
func NewOptimismFinalizer(ctx context.Context, logger *zap.Logger, connector connectors.Connector, l1Finalizer interfaces.L1Finalizer) *OptimismFinalizer {
return &OptimismFinalizer{
logger: logger,
connector: connector,
l1Finalizer: l1Finalizer,
latestFinalizedL2Block: 0,
finalizerMapping: make([]FinalizerEntry, 0),
}
}
func (f *OptimismFinalizer) IsBlockFinalized(ctx context.Context, block *connectors.NewBlock) (bool, error) {
finalizedL1Block := f.l1Finalizer.GetLatestFinalizedBlockNumber()
if finalizedL1Block == 0 {
// This happens on start up.
return false, nil
}
// Result is the json information coming back from the Optimism node's rollup_getInfo() call
type Result struct {
Mode string
EthContext struct {
BlockNumber uint64 `json:"blockNumber"`
TimeStamp uint64 `json:"timestamp"`
} `json:"ethContext"`
RollupContext struct {
Index uint64 `json:"index"`
VerifiedIndex uint64 `json:"verifiedIndex"`
} `json:"rollupContext"`
}
// Always call into the Optimism node to get the latest rollup information so we don't have to wait
// any longer than is necessary for finality by skipping rollup info messages
var info Result
err := f.connector.RawCallContext(ctx, &info, "rollup_getInfo")
if err != nil {
// This is the error case where the RPC call fails
f.logger.Error("failed to get rollup info", zap.String("eth_network", f.connector.NetworkName()), zap.Error(err))
return false, err
}
if info.RollupContext.VerifiedIndex == 0 {
// This is the error case where the RPC call is not working as expected.
return false, fmt.Errorf("Received a verified index of 0. Please check Optimism RPC parameter.")
}
f.logger.Debug("finalizerMapping", zap.Uint64("L2 verified index", info.RollupContext.VerifiedIndex), zap.String(" => ", ""), zap.Uint64("L1_blockNumber", info.EthContext.BlockNumber))
// Look at the last element of the array and see if we need to add this entry
// The assumption here is that every subsequent call moves forward (or stays the same). It is an error if verifiedIndex goes backwards
finalizerMappingSize := len(f.finalizerMapping)
if finalizerMappingSize != 0 && f.finalizerMapping[finalizerMappingSize-1].l2Block > info.RollupContext.VerifiedIndex {
// This is the error case where the RPC call is not working as expected.
return false, fmt.Errorf("The received verified index just went backwards. Received %d. Last number in array is %d", info.RollupContext.VerifiedIndex, f.finalizerMapping[finalizerMappingSize-1].l2Block)
}
if finalizerMappingSize == 0 || f.finalizerMapping[finalizerMappingSize-1].l2Block < info.RollupContext.VerifiedIndex {
// New information. Append it to the array.
f.finalizerMapping = append(f.finalizerMapping, FinalizerEntry{l2Block: info.RollupContext.VerifiedIndex, l1Block: info.EthContext.BlockNumber})
f.logger.Info("Appending new entry.", zap.Int("finalizerMap size", len(f.finalizerMapping)), zap.Uint64("L2 verified index", info.RollupContext.VerifiedIndex), zap.Uint64("L1_blockNumber", info.EthContext.BlockNumber))
}
// Here we want to prune the known finalized entries from the mapping, while recording the latest finalized L2 block number
pruneIdx := -1
for idx, entry := range f.finalizerMapping {
if entry.l1Block > finalizedL1Block {
break
}
// The L1 block for this entry has been finalized so we can prune it.
f.latestFinalizedL2Block = entry.l2Block
pruneIdx = idx
}
if pruneIdx >= 0 {
// Do the pruning here
if pruneIdx+1 >= len(f.finalizerMapping) {
f.finalizerMapping = nil
} else {
f.finalizerMapping = f.finalizerMapping[pruneIdx+1:]
}
f.logger.Info("Pruning finalizerMapping", zap.Int("Pruning from index", pruneIdx), zap.Int("new array size", len(f.finalizerMapping)))
}
isFinalized := block.Number.Uint64() <= f.latestFinalizedL2Block
f.logger.Debug("got rollup info", zap.String("eth_network", f.connector.NetworkName()),
zap.Bool("isFinalized", isFinalized),
zap.String("mode", info.Mode),
zap.Uint64("l1_blockNumber", info.EthContext.BlockNumber),
zap.Uint64("l1_finalizedBlock", finalizedL1Block),
zap.Uint64("l2_blockNumber", info.RollupContext.Index),
zap.Uint64("verified_index", info.RollupContext.VerifiedIndex),
zap.Uint64("latestFinalizedL2Block", f.latestFinalizedL2Block),
zap.Stringer("desired_block", block.Number),
)
return isFinalized, nil
}
/*
curl -X POST --data '{"jsonrpc":"2.0","method":"rollup_getInfo","params":[],"id":1}' https://rpc.ankr.com/optimism_testnet
{
"jsonrpc":"2.0","id":1,"result":{
"mode":"verifier",
"syncing":false,
"ethContext":{
"blockNumber":7763392,"timestamp":1665680949 // This is a few blocks behind the latest block on goerli.
},
"rollupContext":{
"index":1952690,"queueIndex":13285,"verifiedIndex":0 // This is a few blocks behind the latest block on optimism.
}
}
}
*/

View File

@ -257,6 +257,23 @@ func (w *Watcher) Run(ctx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating arbitrum connector failed: %w", err)
}
} else if w.chainID == vaa.ChainIDOptimism && !w.unsafeDevMode {
if w.l1Finalizer == nil {
return fmt.Errorf("unable to create optimism watcher because the l1 finalizer is not set")
}
baseConnector, err := connectors.NewEthereumConnector(timeout, w.networkName, w.url, w.contract, logger)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("dialing eth client failed: %w", err)
}
finalizer := finalizers.NewOptimismFinalizer(timeout, logger, baseConnector, w.l1Finalizer)
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizer, 250*time.Millisecond, false)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating block poll connector failed: %w", err)
}
} else {
w.ethConn, err = connectors.NewEthereumConnector(timeout, w.networkName, w.url, w.contract, logger)
if err != nil {