Optimism finality (#2037)

* node/pkg: update optimism finality

* node/pkg: fix tilt test

* node/pkg: CI changes
This commit is contained in:
Paul Noel 2022-12-02 13:38:45 +00:00 committed by GitHub
parent 22304b94a1
commit 976d8430c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 1608 additions and 52 deletions

View File

@ -159,8 +159,10 @@ var (
arbitrumRPC *string arbitrumRPC *string
arbitrumContract *string arbitrumContract *string
optimismRPC *string optimismRPC *string
optimismContract *string optimismContract *string
optimismCtcRpc *string
optimismCtcContractAddress *string
logLevel *string logLevel *string
@ -298,6 +300,8 @@ func init() {
optimismRPC = NodeCmd.Flags().String("optimismRPC", "", "Optimism RPC URL") optimismRPC = NodeCmd.Flags().String("optimismRPC", "", "Optimism RPC URL")
optimismContract = NodeCmd.Flags().String("optimismContract", "", "Optimism contract address") optimismContract = NodeCmd.Flags().String("optimismContract", "", "Optimism contract address")
optimismCtcRpc = NodeCmd.Flags().String("optimismCtcRpc", "", "Optimism CTC RPC")
optimismCtcContractAddress = NodeCmd.Flags().String("optimismCtcContractAddress", "", "Optimism CTC contract address")
logLevel = NodeCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)") logLevel = NodeCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)")
@ -1078,11 +1082,19 @@ func runNode(cmd *cobra.Command, args []string) {
if ethWatcher == nil { if ethWatcher == nil {
log.Fatalf("if optimism is enabled then ethereum must also be enabled.") log.Fatalf("if optimism is enabled then ethereum must also be enabled.")
} }
if !*unsafeDevMode {
if *optimismCtcRpc == "" || *optimismCtcContractAddress == "" {
log.Fatalf("--optimismCtcRpc and --optimismCtcContractAddress both need to be set.")
}
}
logger.Info("Starting Optimism watcher") logger.Info("Starting Optimism watcher")
readiness.RegisterComponent(common.ReadinessOptimismSyncing) readiness.RegisterComponent(common.ReadinessOptimismSyncing)
chainObsvReqC[vaa.ChainIDOptimism] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize) chainObsvReqC[vaa.ChainIDOptimism] = make(chan *gossipv1.ObservationRequest, observationRequestBufferSize)
optimismWatcher := evm.NewEthWatcher(*optimismRPC, optimismContractAddr, "optimism", common.ReadinessOptimismSyncing, vaa.ChainIDOptimism, lockC, nil, chainObsvReqC[vaa.ChainIDOptimism], *unsafeDevMode) optimismWatcher := evm.NewEthWatcher(*optimismRPC, optimismContractAddr, "optimism", common.ReadinessOptimismSyncing, vaa.ChainIDOptimism, lockC, nil, chainObsvReqC[vaa.ChainIDOptimism], *unsafeDevMode)
optimismWatcher.SetL1Finalizer(ethWatcher) optimismWatcher.SetL1Finalizer(ethWatcher)
if err := optimismWatcher.SetRootChainParams(*optimismCtcRpc, *optimismCtcContractAddress); err != nil {
return err
}
if err := supervisor.Run(ctx, "optimismwatch", optimismWatcher.Run); err != nil { if err := supervisor.Run(ctx, "optimismwatch", optimismWatcher.Run); err != nil {
return err return err
} }

View File

@ -1,12 +1,28 @@
// Optimism has a Canonical Transaction Chain (CTC) contract running on the L1.
// This contract contains information about the mapping of L2 => L1 blocks.
// The Finalizer queries that information and places it in an array.
// This allows the finalizer to map an L2 block to an L1 block.
// Then the finalizer can query the L1 chain directly to see if the related L1 block is finalized.
// CTC mainnet contract: 0x5E4e65926BA27467555EB562121fac00D24E9dD2
// CTC testnet contract: 0x607F755149cFEB3a14E1Dc3A4E2450Cde7dfb04D
package finalizers package finalizers
import ( import (
"context" "context"
"fmt" "fmt"
"math/big"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors" "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
ctcAbi "github.com/certusone/wormhole/node/pkg/watchers/evm/finalizers/optimismctcabi"
"github.com/certusone/wormhole/node/pkg/watchers/interfaces" "github.com/certusone/wormhole/node/pkg/watchers/interfaces"
ethBind "github.com/ethereum/go-ethereum/accounts/abi/bind"
ethCommon "github.com/ethereum/go-ethereum/common"
ethClient "github.com/ethereum/go-ethereum/ethclient"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -14,82 +30,122 @@ import (
// Optimism provides a special "rollup_getInfo" API call to determine the latest L2 (Optimism) block to be published on the L1 (Ethereum). // 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. // This finalizer polls that API to determine if a block is finalized.
type FinalizerEntry struct {
l2Block uint64
l1Block uint64
}
type OptimismFinalizer struct { type OptimismFinalizer struct {
logger *zap.Logger logger *zap.Logger
connector connectors.Connector connector connectors.Connector
l1Finalizer interfaces.L1Finalizer l1Finalizer interfaces.L1Finalizer
latestFinalizedL2Block uint64 latestFinalizedL2Block *big.Int
// finalizerMapping is a array of FinalizerEntry structs with the L2 block number that has been verified and its corresponding L1 block number // finalizerMapping is a array of RollupInfo structs with the L2 block number that has been verified and its corresponding L1 block number
finalizerMapping []FinalizerEntry finalizerMapping []RollupInfo
// These are used for querying the ctc contract.
ctcRawClient *ethRpc.Client
ctcClient *ethClient.Client
// This is used to grab the rollup information from the ctc contract
ctcCaller *ctcAbi.OptimismCtcAbiCaller
} }
func NewOptimismFinalizer(ctx context.Context, logger *zap.Logger, connector connectors.Connector, l1Finalizer interfaces.L1Finalizer) *OptimismFinalizer { func NewOptimismFinalizer(
return &OptimismFinalizer{ ctx context.Context,
logger *zap.Logger,
connector connectors.Connector,
l1Finalizer interfaces.L1Finalizer,
ctcChainUrl string,
ctcChainAddress string,
) (*OptimismFinalizer, error) {
ctcRawClient, err := ethRpc.DialContext(ctx, ctcChainUrl)
if err != nil {
return nil, fmt.Errorf("failed to create raw client for url %s: %w", ctcChainUrl, err)
}
ctcClient := ethClient.NewClient(ctcRawClient)
addr := ethCommon.HexToAddress(ctcChainAddress)
ctcCaller, err := ctcAbi.NewOptimismCtcAbiCaller(addr, ctcClient)
if err != nil {
return nil, fmt.Errorf("failed to create ctc caller for url %s: %w", ctcChainUrl, err)
}
finalizer := &OptimismFinalizer{
logger: logger, logger: logger,
connector: connector, connector: connector,
l1Finalizer: l1Finalizer, l1Finalizer: l1Finalizer,
latestFinalizedL2Block: 0, latestFinalizedL2Block: big.NewInt(0),
finalizerMapping: make([]FinalizerEntry, 0), finalizerMapping: make([]RollupInfo, 0),
ctcRawClient: ctcRawClient,
ctcClient: ctcClient,
ctcCaller: ctcCaller,
} }
return finalizer, nil
}
// Both types are big.Int because that is what the abi returns.
// However, there are 2 points to note:
// The L1 block number is a uint40 in the abi. So, safe to convert to Uint64.
// The L2 block number is a uint256 in the abi. So, this is never converted to anything else.
type RollupInfo struct {
l2Block *big.Int
l1Block *big.Int
}
func (f *OptimismFinalizer) GetRollupInfo(ctx context.Context) (RollupInfo, error) {
// Get the current latest blocks.
opts := &ethBind.CallOpts{Context: ctx}
var entry RollupInfo
l2Block, err := f.ctcCaller.GetTotalElements(opts)
if err != nil {
return entry, fmt.Errorf("failed to get L2 block: %w", err)
}
l1Block, err := f.ctcCaller.GetLastBlockNumber(opts)
if err != nil {
return entry, fmt.Errorf("failed to get L1 block: %w", err)
}
entry.l1Block = l1Block
entry.l2Block = l2Block
f.logger.Debug("GetRollupInfo", zap.Stringer("l1Block", entry.l1Block), zap.Stringer("l2Block", entry.l2Block))
return entry, nil
} }
func (f *OptimismFinalizer) IsBlockFinalized(ctx context.Context, block *connectors.NewBlock) (bool, error) { func (f *OptimismFinalizer) IsBlockFinalized(ctx context.Context, block *connectors.NewBlock) (bool, error) {
finalizedL1Block := f.l1Finalizer.GetLatestFinalizedBlockNumber() finalizedL1Block := f.l1Finalizer.GetLatestFinalizedBlockNumber() // Uint64
if finalizedL1Block == 0 { if finalizedL1Block == 0 {
// This happens on start up. // This happens on start up.
return false, nil 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 // 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 // any longer than is necessary for finality by skipping rollup info messages
var info Result rInfo, err := f.GetRollupInfo(ctx)
err := f.connector.RawCallContext(ctx, &info, "rollup_getInfo")
if err != nil { if err != nil {
// This is the error case where the RPC call fails // This is the error case where the contract call fails
f.logger.Error("failed to get rollup info", zap.String("eth_network", f.connector.NetworkName()), zap.Error(err)) f.logger.Error("failed to get rollup info", zap.Error(err))
return false, 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)) f.logger.Debug("finalizerMapping", zap.String("L2 block", rInfo.l2Block.String()), zap.String("=> L1 block", rInfo.l1Block.String()))
// Look at the last element of the array and see if we need to add this entry // 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 // 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) finalizerMappingSize := len(f.finalizerMapping)
if finalizerMappingSize != 0 && f.finalizerMapping[finalizerMappingSize-1].l2Block > info.RollupContext.VerifiedIndex { if finalizerMappingSize != 0 && f.finalizerMapping[finalizerMappingSize-1].l2Block.Cmp(rInfo.l2Block) > 0 {
// This is the error case where the RPC call is not working as expected. // 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) return false, fmt.Errorf("The received verified index just went backwards. Received %s. Last number in array is %s", rInfo.l2Block.String(), f.finalizerMapping[finalizerMappingSize-1].l2Block.String())
} }
if finalizerMappingSize == 0 || f.finalizerMapping[finalizerMappingSize-1].l2Block < info.RollupContext.VerifiedIndex { if finalizerMappingSize == 0 || f.finalizerMapping[finalizerMappingSize-1].l2Block.Cmp(rInfo.l2Block) < 0 {
// New information. Append it to the array. // New information. Append it to the array.
f.finalizerMapping = append(f.finalizerMapping, FinalizerEntry{l2Block: info.RollupContext.VerifiedIndex, l1Block: info.EthContext.BlockNumber}) f.finalizerMapping = append(f.finalizerMapping, rInfo)
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)) f.logger.Info("Appending new entry.", zap.Int("finalizerMap size", len(f.finalizerMapping)), zap.String("L2 block number", rInfo.l2Block.String()), zap.String("L1 block number", rInfo.l1Block.String()))
} }
// Here we want to prune the known finalized entries from the mapping, while recording the latest finalized L2 block number // Here we want to prune the known finalized entries from the mapping, while recording the latest finalized L2 block number
pruneIdx := -1 pruneIdx := -1
for idx, entry := range f.finalizerMapping { for idx, entry := range f.finalizerMapping {
if entry.l1Block > finalizedL1Block { if entry.l1Block.Uint64() > finalizedL1Block {
break break
} }
// The L1 block for this entry has been finalized so we can prune it. // The L1 block for this entry has been finalized so we can prune it.
@ -103,19 +159,16 @@ func (f *OptimismFinalizer) IsBlockFinalized(ctx context.Context, block *connect
} else { } else {
f.finalizerMapping = f.finalizerMapping[pruneIdx+1:] 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))) f.logger.Debug("Pruning finalizerMapping", zap.Int("Pruning from index", pruneIdx), zap.Int("new array size", len(f.finalizerMapping)))
} }
isFinalized := block.Number.Uint64() <= f.latestFinalizedL2Block isFinalized := block.Number.Cmp(f.latestFinalizedL2Block) <= 0
f.logger.Debug("got rollup info", zap.String("eth_network", f.connector.NetworkName()), f.logger.Debug("got rollup info",
zap.Bool("isFinalized", isFinalized), zap.Uint64("l1_blockNumber", rInfo.l1Block.Uint64()),
zap.String("mode", info.Mode),
zap.Uint64("l1_blockNumber", info.EthContext.BlockNumber),
zap.Uint64("l1_finalizedBlock", finalizedL1Block), zap.Uint64("l1_finalizedBlock", finalizedL1Block),
zap.Uint64("l2_blockNumber", info.RollupContext.Index), zap.String("l2_blockNumber", rInfo.l2Block.String()),
zap.Uint64("verified_index", info.RollupContext.VerifiedIndex), zap.String("latestFinalizedL2Block", f.latestFinalizedL2Block.String()),
zap.Uint64("latestFinalizedL2Block", f.latestFinalizedL2Block),
zap.Stringer("desired_block", block.Number), zap.Stringer("desired_block", block.Number),
) )

File diff suppressed because one or more lines are too long

View File

@ -288,7 +288,11 @@ func (w *Watcher) Run(ctx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1) p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("dialing eth client failed: %w", err) return fmt.Errorf("dialing eth client failed: %w", err)
} }
finalizer := finalizers.NewOptimismFinalizer(timeout, logger, baseConnector, w.l1Finalizer) finalizer, err := finalizers.NewOptimismFinalizer(timeout, logger, baseConnector, w.l1Finalizer, w.rootChainRpc, w.rootChainContract)
if err != nil {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating optimism finalizer failed: %w", err)
}
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizer, 250*time.Millisecond, false, false) w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizer, 250*time.Millisecond, false, false)
if err != nil { if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc() ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()