wormhole/node/pkg/watchers/evm/finalizers/optimism.go

199 lines
7.3 KiB
Go

// 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
import (
"context"
"fmt"
"math/big"
"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"
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"
)
// 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 OptimismFinalizer struct {
logger *zap.Logger
l1Finalizer interfaces.L1Finalizer
latestFinalizedL2Block *big.Int
// finalizerMapping is a array of RollupInfo structs with the L2 block number that has been verified and its corresponding L1 block number
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 ctcCallerIntf
}
type ctcCallerIntf interface {
GetTotalElements(opts *ethBind.CallOpts) (*big.Int, error)
GetLastBlockNumber(opts *ethBind.CallOpts) (*big.Int, error)
}
func NewOptimismFinalizer(
ctx context.Context,
logger *zap.Logger,
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,
l1Finalizer: l1Finalizer,
latestFinalizedL2Block: big.NewInt(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) {
if block == nil {
return false, fmt.Errorf("block is nil")
}
finalizedL1Block := f.l1Finalizer.GetLatestFinalizedBlockNumber() // Uint64
if finalizedL1Block == 0 {
// This happens on start up.
return false, nil
}
// 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
rInfo, err := f.GetRollupInfo(ctx)
if err != nil {
// This is the error case where the contract call fails
f.logger.Error("failed to get rollup info", zap.Error(err))
return false, err
}
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
// 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.Cmp(rInfo.l2Block) > 0 {
// 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 %s. Last number in array is %s", rInfo.l2Block.String(), f.finalizerMapping[finalizerMappingSize-1].l2Block.String())
}
if finalizerMappingSize == 0 || f.finalizerMapping[finalizerMappingSize-1].l2Block.Cmp(rInfo.l2Block) < 0 {
// New information. Append it to the array.
f.finalizerMapping = append(f.finalizerMapping, rInfo)
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
pruneIdx := -1
for idx, entry := range f.finalizerMapping {
if entry.l1Block.Uint64() > 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.Debug("Pruning finalizerMapping", zap.Int("Pruning from index", pruneIdx), zap.Int("new array size", len(f.finalizerMapping)))
}
isFinalized := block.Number.Cmp(f.latestFinalizedL2Block) <= 0
f.logger.Debug("got rollup info",
zap.Uint64("l1_blockNumber", rInfo.l1Block.Uint64()),
zap.Uint64("l1_finalizedBlock", finalizedL1Block),
zap.String("l2_blockNumber", rInfo.l2Block.String()),
zap.String("latestFinalizedL2Block", f.latestFinalizedL2Block.String()),
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.
}
}
}
*/