Node/EVM: More nodes support finalized and safe (#3467)

* Node/EVM: More nodes support finalized and safe

* Remove unused finalizers
This commit is contained in:
bruce-riley 2023-10-26 14:26:15 -05:00 committed by GitHub
parent 5afd9eab2d
commit bd7262d819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 74 additions and 475 deletions

View File

@ -1079,11 +1079,10 @@ func runNode(cmd *cobra.Command, args []string) {
if shouldStart(bscRPC) {
wc := &evm.WatcherConfig{
NetworkID: "bsc",
ChainID: vaa.ChainIDBSC,
Rpc: *bscRPC,
Contract: *bscContract,
WaitForConfirmations: true,
NetworkID: "bsc",
ChainID: vaa.ChainIDBSC,
Rpc: *bscRPC,
Contract: *bscContract,
}
watcherConfigs = append(watcherConfigs, wc)

View File

@ -1,46 +0,0 @@
package finalizers
import (
"context"
"fmt"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
"github.com/certusone/wormhole/node/pkg/watchers/interfaces"
"go.uber.org/zap"
)
// ArbitrumFinalizer implements the finality check for Arbitrum.
// Arbitrum blocks should not be considered finalized until they are finalized on Ethereum.
type ArbitrumFinalizer struct {
logger *zap.Logger
l1Finalizer interfaces.L1Finalizer
}
func NewArbitrumFinalizer(logger *zap.Logger, l1Finalizer interfaces.L1Finalizer) *ArbitrumFinalizer {
return &ArbitrumFinalizer{
logger: logger,
l1Finalizer: l1Finalizer,
}
}
// IsBlockFinalized compares the number of the L1 block containing the Arbitrum block with the latest finalized block on Ethereum.
func (a *ArbitrumFinalizer) IsBlockFinalized(ctx context.Context, block *connectors.NewBlock) (bool, error) {
if block == nil {
return false, fmt.Errorf("block is nil")
}
if block.L1BlockNumber == nil {
return false, fmt.Errorf("l1 block number is nil")
}
latestL1Block := a.l1Finalizer.GetLatestFinalizedBlockNumber()
if latestL1Block == 0 {
// This happens on start up.
return false, nil
}
isFinalized := block.L1BlockNumber.Uint64() <= latestL1Block
return isFinalized, nil
}

View File

@ -1,131 +0,0 @@
package finalizers
import (
"context"
"math/big"
"testing"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
// mockL1Finalizer implements the L1Finalizer interface for testing purposes.
type mockL1Finalizer struct {
LatestFinalizedBlockNumber uint64
}
func (m *mockL1Finalizer) GetLatestFinalizedBlockNumber() uint64 {
return m.LatestFinalizedBlockNumber
}
func TestArbitrumErrorReturnedIfBlockIsNil(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{LatestFinalizedBlockNumber: 125}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
_, err := finalizer.IsBlockFinalized(ctx, nil)
require.Error(t, err)
}
func TestArbitrumErrorReturnedIfL1BlockIsNil(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{LatestFinalizedBlockNumber: 125}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.Hash{},
L1BlockNumber: nil,
}
_, err := finalizer.IsBlockFinalized(ctx, block)
require.Error(t, err)
}
func TestArbitrumNotFinalizedIfNoFinalizedL1BlockYet(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.Hash{},
L1BlockNumber: big.NewInt(225),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, false, finalized)
}
func TestArbitrumNotFinalizedWhenFinalizedL1IsLessThanTargetL1(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{LatestFinalizedBlockNumber: 225}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(127),
Hash: ethCommon.Hash{},
L1BlockNumber: big.NewInt(226),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, false, finalized)
}
func TestArbitrumIsFinalizedWhenFinalizedL1IsEqualsTargetL1(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{LatestFinalizedBlockNumber: 225}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.Hash{},
L1BlockNumber: big.NewInt(225),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, true, finalized)
}
func TestArbitrumIsFinalizedWhenFinalizedL1IsGreaterThanTargetL1(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
l1Finalizer := mockL1Finalizer{LatestFinalizedBlockNumber: 227}
finalizer := NewArbitrumFinalizer(logger, &l1Finalizer)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.Hash{},
L1BlockNumber: big.NewInt(225),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, true, finalized)
}

View File

@ -1,41 +0,0 @@
package finalizers
import (
"context"
"fmt"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
"go.uber.org/zap"
)
// MoonbeamFinalizer implements the finality check for Moonbeam.
// Moonbeam can publish blocks before they are marked final. This means we need to sit on the block until a special "is finalized"
// query returns true. The assumption is that every block number will eventually be published and finalized, it's just that the contents
// of the block (and therefore the hash) might change if there is a rollback.
type MoonbeamFinalizer struct {
logger *zap.Logger
connector connectors.Connector
}
func NewMoonbeamFinalizer(logger *zap.Logger, connector connectors.Connector) *MoonbeamFinalizer {
return &MoonbeamFinalizer{
logger: logger,
connector: connector,
}
}
func (m *MoonbeamFinalizer) IsBlockFinalized(ctx context.Context, block *connectors.NewBlock) (bool, error) {
if block == nil {
return false, fmt.Errorf("block is nil")
}
var finalized bool
err := m.connector.RawCallContext(ctx, &finalized, "moon_isBlockFinalized", block.Hash.Hex())
if err != nil {
m.logger.Error("failed to check for finality", zap.String("eth_network", m.connector.NetworkName()), zap.Error(err))
return false, err
}
return finalized, nil
}

View File

@ -1,137 +0,0 @@
package finalizers
import (
"context"
"encoding/json"
"fmt"
"math/big"
"testing"
"github.com/certusone/wormhole/node/pkg/watchers/evm/connectors"
ethAbi "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi"
ethereum "github.com/ethereum/go-ethereum"
ethCommon "github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
ethEvent "github.com/ethereum/go-ethereum/event"
ethRpc "github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
type moonbeamMockConnector struct {
isFinalized string
err error
}
func (e *moonbeamMockConnector) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) (err error) {
if method != "moon_isBlockFinalized" {
panic("method not implemented by moonbeamMockConnector")
}
err = json.Unmarshal([]byte(e.isFinalized), &result)
return
}
func (e *moonbeamMockConnector) RawBatchCallContext(ctx context.Context, b []ethRpc.BatchElem) error {
panic("method not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) NetworkName() string {
return "moonbeamMockConnector"
}
func (e *moonbeamMockConnector) ContractAddress() ethCommon.Address {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) GetCurrentGuardianSetIndex(ctx context.Context) (uint32, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) GetGuardianSet(ctx context.Context, index uint32) (ethAbi.StructsGuardianSet, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) WatchLogMessagePublished(ctx context.Context, errC chan error, sink chan<- *ethAbi.AbiLogMessagePublished) (ethEvent.Subscription, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) TransactionReceipt(ctx context.Context, txHash ethCommon.Hash) (*ethTypes.Receipt, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) TimeOfBlockByHash(ctx context.Context, hash ethCommon.Hash) (uint64, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) ParseLogMessagePublished(log ethTypes.Log) (*ethAbi.AbiLogMessagePublished, error) {
panic("not implemented by moonbeamMockConnector")
}
func (e *moonbeamMockConnector) SubscribeForBlocks(ctx context.Context, errC chan error, sink chan<- *connectors.NewBlock) (ethereum.Subscription, error) {
panic("not implemented by moonbeamMockConnector")
}
func TestMoonbeamErrorReturnedIfBlockIsNil(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
baseConnector := moonbeamMockConnector{isFinalized: "true", err: nil}
finalizer := NewMoonbeamFinalizer(logger, &baseConnector)
assert.NotNil(t, finalizer)
_, err := finalizer.IsBlockFinalized(ctx, nil)
require.Error(t, err)
}
func TestMoonbeamBlockNotFinalized(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
baseConnector := moonbeamMockConnector{isFinalized: "false", err: nil}
finalizer := NewMoonbeamFinalizer(logger, &baseConnector)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.HexToHash("0x1076cd8c207f31e1638b37bb358c458f216f5451f06e2ccb4eb9db66ad669f30"),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, false, finalized)
}
func TestMoonbeamBlockIsFinalized(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
baseConnector := moonbeamMockConnector{isFinalized: "true", err: nil}
finalizer := NewMoonbeamFinalizer(logger, &baseConnector)
assert.NotNil(t, finalizer)
block := &connectors.NewBlock{
Number: big.NewInt(125),
Hash: ethCommon.HexToHash("0x1076cd8c207f31e1638b37bb358c458f216f5451f06e2ccb4eb9db66ad669f30"),
}
finalized, err := finalizer.IsBlockFinalized(ctx, block)
require.NoError(t, err)
assert.Equal(t, true, finalized)
}
func TestMoonbeamRpcError(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()
baseConnector := moonbeamMockConnector{isFinalized: "true", err: fmt.Errorf("RPC failed")}
finalizer := NewMoonbeamFinalizer(logger, &baseConnector)
assert.NotNil(t, finalizer)
_, err := finalizer.IsBlockFinalized(ctx, nil)
require.Error(t, err)
}

View File

@ -15,6 +15,15 @@ import (
"go.uber.org/zap"
)
// mockL1Finalizer implements the L1Finalizer interface for testing purposes.
type mockL1Finalizer struct {
LatestFinalizedBlockNumber uint64
}
func (m *mockL1Finalizer) GetLatestFinalizedBlockNumber() uint64 {
return m.LatestFinalizedBlockNumber
}
func TestNeonErrorReturnedIfBlockIsNil(t *testing.T) {
ctx := context.Background()
logger := zap.NewNop()

View File

@ -180,6 +180,7 @@ func NewEthWatcher(
}
func (w *Watcher) Run(parentCtx context.Context) error {
var err error
logger := supervisor.Logger(parentCtx)
logger.Info("Starting watcher",
@ -197,16 +198,10 @@ func (w *Watcher) Run(parentCtx context.Context) error {
ctx, watcherContextCancelFunc := context.WithCancel(parentCtx)
defer watcherContextCancelFunc()
useFinalizedBlocks := ((w.chainID == vaa.ChainIDEthereum || w.chainID == vaa.ChainIDSepolia) && (!w.unsafeDevMode))
if (w.chainID == vaa.ChainIDKarura || w.chainID == vaa.ChainIDAcala) && (!w.unsafeDevMode) {
ufb, err := w.getAcalaMode(ctx)
if err != nil {
return err
}
if ufb {
useFinalizedBlocks = true
}
var useFinalizedBlocks, safeBlocksSupported bool
useFinalizedBlocks, safeBlocksSupported, err = w.getFinality(ctx)
if err != nil {
return fmt.Errorf("failed to determine finality: %w", err)
}
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
@ -217,21 +212,8 @@ func (w *Watcher) Run(parentCtx context.Context) error {
timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
safeBlocksSupported := false
var err error
if w.chainID == vaa.ChainIDCelo && !w.unsafeDevMode {
// When we are running in mainnet or testnet, we need to use the Celo ethereum library rather than go-ethereum.
// However, in devnet, we currently run the standard ETH node for Celo, so we need to use the standard go-ethereum.
w.ethConn, err = connectors.NewCeloConnector(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)
}
} else if useFinalizedBlocks {
if (w.chainID == vaa.ChainIDEthereum || w.chainID == vaa.ChainIDSepolia) && !w.unsafeDevMode {
safeBlocksSupported = true
if useFinalizedBlocks {
if safeBlocksSupported {
logger.Info("using finalized blocks, will publish safe blocks")
} else {
logger.Info("using finalized blocks")
@ -249,21 +231,17 @@ func (w *Watcher) Run(parentCtx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating block poll connector failed: %w", err)
}
} else if w.chainID == vaa.ChainIDMoonbeam && !w.unsafeDevMode {
baseConnector, err := connectors.NewEthereumConnector(timeout, w.networkName, w.url, w.contract, logger)
} else if w.chainID == vaa.ChainIDCelo {
// When we are running in mainnet or testnet, we need to use the Celo ethereum library rather than go-ethereum.
// However, in devnet, we currently run the standard ETH node for Celo, so we need to use the standard go-ethereum.
w.ethConn, err = connectors.NewCeloConnector(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.NewMoonbeamFinalizer(logger, baseConnector)
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizer, 250*time.Millisecond, false, 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 if w.chainID == vaa.ChainIDNeon && !w.unsafeDevMode {
} else if w.chainID == vaa.ChainIDNeon {
// Neon needs special handling to read log events.
if w.l1Finalizer == nil {
return fmt.Errorf("unable to create neon watcher because the l1 finalizer is not set")
}
@ -286,41 +264,8 @@ func (w *Watcher) Run(parentCtx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating poll connector failed: %w", err)
}
} else if w.chainID == vaa.ChainIDArbitrum && !w.unsafeDevMode {
if w.l1Finalizer == nil {
return fmt.Errorf("unable to create arbitrum 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.NewArbitrumFinalizer(logger, w.l1Finalizer)
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizer, 250*time.Millisecond, false, false)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating arbitrum connector failed: %w", err)
}
} else if w.chainID == vaa.ChainIDOptimism && !w.unsafeDevMode {
// This only supports Bedrock mode
useFinalizedBlocks = true
safeBlocksSupported = true
logger.Info("using finalized blocks, will publish safe blocks")
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)
}
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizers.NewDefaultFinalizer(), 250*time.Millisecond, useFinalizedBlocks, safeBlocksSupported)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating optimism connector failed: %w", err)
}
} else if w.chainID == vaa.ChainIDPolygon && w.usePolygonCheckpointing() {
// Polygon polls the root contract on Ethereum.
baseConnector, err := connectors.NewEthereumConnector(timeout, w.networkName, w.url, w.contract, logger)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
@ -337,22 +282,8 @@ func (w *Watcher) Run(parentCtx context.Context) error {
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("failed to create polygon connector: %w", err)
}
} else if w.chainID == vaa.ChainIDBase && !w.unsafeDevMode {
useFinalizedBlocks = true
safeBlocksSupported = true
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)
}
w.ethConn, err = connectors.NewBlockPollConnector(ctx, baseConnector, finalizers.NewDefaultFinalizer(), 250*time.Millisecond, useFinalizedBlocks, safeBlocksSupported)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
p2p.DefaultRegistry.AddErrorCount(w.chainID, 1)
return fmt.Errorf("creating base connector failed: %w", err)
}
} else {
// Everything else is instant finality.
w.ethConn, err = connectors.NewEthereumConnector(timeout, w.networkName, w.url, w.contract, logger)
if err != nil {
ethConnectionErrors.WithLabelValues(w.networkName, "dial_error").Inc()
@ -889,41 +820,56 @@ func fetchCurrentGuardianSet(ctx context.Context, ethConn connectors.Connector)
return currentIndex, &gs, nil
}
func (w *Watcher) getAcalaMode(ctx context.Context) (useFinalizedBlocks bool, errRet error) {
timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
c, err := rpc.DialContext(timeout, w.url)
if err != nil {
errRet = fmt.Errorf("failed to connect to url %s to check acala mode: %w", w.url, err)
return
// getFinality determines if the chain supports "finalized" and "safe". This is hard coded so it requires thought to change something. However, it also reads the RPC
// to make sure the node actually supports the expected values, and returns an error if it doesn't. Note that we do not support using safe mode but not finalized mode.
func (w *Watcher) getFinality(ctx context.Context) (bool, bool, error) {
var finalized, safe bool
if w.unsafeDevMode {
// Devnet supports finalized and safe (although they returns the same value as latest).
finalized = true
safe = true
} else if w.chainID == vaa.ChainIDAcala ||
w.chainID == vaa.ChainIDArbitrum ||
w.chainID == vaa.ChainIDBase ||
w.chainID == vaa.ChainIDBSC ||
w.chainID == vaa.ChainIDEthereum ||
w.chainID == vaa.ChainIDKarura ||
w.chainID == vaa.ChainIDMoonbeam ||
w.chainID == vaa.ChainIDOptimism ||
w.chainID == vaa.ChainIDSepolia {
finalized = true
safe = true
}
// First check to see if polling for finalized blocks is suported.
type Marshaller struct {
Number *eth_hexutil.Big
// If finalized / safe should be supported, read the RPC to make sure they actually are.
if finalized {
timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
c, err := rpc.DialContext(timeout, w.url)
if err != nil {
return false, false, fmt.Errorf("failed to connect to endpoint: %w", err)
}
type Marshaller struct {
Number *eth_hexutil.Big
}
var m Marshaller
err = c.CallContext(ctx, &m, "eth_getBlockByNumber", "finalized", false)
if err != nil {
return false, false, fmt.Errorf("finalized not supported by the node when it should be: %w", err)
}
if safe {
err = c.CallContext(ctx, &m, "eth_getBlockByNumber", "safe", false)
if err != nil {
return false, false, fmt.Errorf("safe not supported by the node when it should be: %w", err)
}
}
}
var m Marshaller
err = c.CallContext(ctx, &m, "eth_getBlockByNumber", "finalized", false)
if err == nil {
useFinalizedBlocks = true
return
}
// If finalized blocks are not supported, then we had better be in safe mode!
var safe bool
err = c.CallContext(ctx, &safe, "net_isSafeMode")
if err != nil {
errRet = fmt.Errorf("check for safe mode for url %s failed: %w", w.url, err)
return
}
if !safe {
errRet = fmt.Errorf("url %s does not support finalized blocks and is not using safe mode", w.url)
}
return
return finalized, safe, nil
}
// SetL1Finalizer is used to set the layer one finalizer.