// 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 := ðBind.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. } } } */