From 41bc9fb31f36c304fbec47051c5c2df5150f2db8 Mon Sep 17 00:00:00 2001 From: Ethen Pociask Date: Wed, 28 Feb 2024 15:36:39 -0700 Subject: [PATCH] Withdrawal safety for all key L2->L1 events (#186) Co-authored-by: Ethen Pociask Co-authored-by: Adrian Smith --- e2e/heuristic_test.go | 119 +++++++--- internal/api/models/heuristic.go | 2 +- internal/core/core.go | 4 +- internal/core/core_test.go | 2 +- ...awal_safety.go => l1_withdrawal_safety.go} | 174 +++++--------- .../engine/registry/l2_withdrawal_safety.go | 223 ++++++++++++++++++ internal/engine/registry/registry.go | 41 ++-- internal/engine/registry/registry_test.go | 6 +- 8 files changed, 398 insertions(+), 173 deletions(-) rename internal/engine/registry/{withdrawal_safety.go => l1_withdrawal_safety.go} (52%) create mode 100644 internal/engine/registry/l2_withdrawal_safety.go diff --git a/e2e/heuristic_test.go b/e2e/heuristic_test.go index 6f075087..daae6719 100644 --- a/e2e/heuristic_test.go +++ b/e2e/heuristic_test.go @@ -224,7 +224,7 @@ func TestContractEvent(t *testing.T) { // TestWithdrawalSafetyAllInvariants ... Tests the E2E flow of a withdrawal // safety heuristic session. This test ensures that an alert is produced in the event -// of a highly suspicious withdrawal. +// of a highly suspicious withdrawal at every step of the withdrawal flow. func TestWithdrawalSafetyAllInvariants(t *testing.T) { ts := e2e.CreateSysTestSuite(t, "") defer ts.Close() @@ -259,6 +259,22 @@ func TestWithdrawalSafetyAllInvariants(t *testing.T) { core.L2ToL1MessagePasser: fakeAddr.String(), }, }, + { + Network: core.Layer2.String(), + HeuristicType: core.WithdrawalSafety.String(), + StartHeight: nil, + EndHeight: nil, + AlertingParams: &core.AlertPolicy{ + Sev: core.LOW.String(), + Msg: alertMsg, + }, + SessionParams: map[string]interface{}{ + "threshold": 0.20, + "coefficient_threshold": 0.20, + core.L1Portal: ts.Cfg.L1Deployments.OptimismPortalProxy.String(), + core.L2ToL1MessagePasser: predeploys.L2ToL1MessagePasserAddr.String(), + }, + }, }) require.NoError(t, err, "Error bootstrapping heuristic session") @@ -284,12 +300,33 @@ func TestWithdrawalSafetyAllInvariants(t *testing.T) { _, err = wait.ForReceiptOK(context.Background(), ts.L1Client, depositTx.Hash()) require.NoError(t, err) - // Initiate and prove a withdrawal + // Initiate withdrawal withdrawTx, err := l2ToL1MessagePasser.InitiateWithdrawal(l2Opts, aliceAddr, big.NewInt(100_000), calldata) require.NoError(t, err) - withdrawReceipt, err := wait.ForReceiptOK(context.Background(), ts.L2Client, withdrawTx.Hash()) + initReceipt, err := wait.ForReceiptOK(context.Background(), ts.L2Client, withdrawTx.Hash()) require.NoError(t, err) + // Wait for Pessimism to process initiation + require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { + id := ids[0].PathID + height, err := ts.Subsystems.PathHeight(id) + if err != nil { + return false, err + } + + return height != nil && height.Uint64() > initReceipt.BlockNumber.Uint64(), nil + })) + + // Ensure Pessimism has detected what it considers an unsafe withdrawal + alerts := ts.TestSlackSvr.SlackAlerts() + require.Equal(t, 1, len(alerts), "expected 1 alerts") + assert.Contains(t, alerts[0].Text, core.WithdrawalSafety.String(), "expected alert to be for withdrawal_safety") + assert.Contains(t, alerts[0].Text, alertMsg, "expected alert to have alert message") + + // Ensure that specific invariant messages are included in the alert + assert.Contains(t, alerts[0].Text, alertMsg, registry.GreaterThanPortal) + + ts.TestSlackSvr.ClearAlerts() // Mock the indexer call to return a really high withdrawal amount ts.TestIxClient.EXPECT().GetAllWithdrawalsByAddress(gomock.Any()).Return([]api_mods.WithdrawalItem{ { @@ -298,7 +335,7 @@ func TestWithdrawalSafetyAllInvariants(t *testing.T) { }, }, nil).AnyTimes() - _, proveReceipt := op_e2e.ProveWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Sys.EthInstances["sequencer"], ts.Cfg.Secrets.Alice, withdrawReceipt) + params, proveReceipt := op_e2e.ProveWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Sys.EthInstances["sequencer"], ts.Cfg.Secrets.Alice, initReceipt) // Wait for Pessimism to process the proven withdrawal and send a notification to the mocked Slack server. require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { @@ -312,7 +349,7 @@ func TestWithdrawalSafetyAllInvariants(t *testing.T) { })) // Ensure Pessimism has detected what it considers an unsafe withdrawal - alerts := ts.TestSlackSvr.SlackAlerts() + alerts = ts.TestSlackSvr.SlackAlerts() require.Equal(t, 1, len(alerts), "expected 1 alerts") assert.Contains(t, alerts[0].Text, core.WithdrawalSafety.String(), "expected alert to be for withdrawal_safety") assert.Contains(t, alerts[0].Text, fakeAddr.String(), "expected alert to be for dummy L2ToL1MessagePasser") @@ -323,34 +360,34 @@ func TestWithdrawalSafetyAllInvariants(t *testing.T) { assert.Contains(t, alerts[0].Text, alertMsg, registry.GreaterThanPortal) assert.Contains(t, alerts[0].Text, alertMsg, fmt.Sprintf(registry.GreaterThanThreshold, 20.0)) - // TODO(#178) - Feat - Support WithdrawalProven processing in withdrawal_safety heuristic - // Mock the indexer call to return a really low withdrawal amount - // ts.TestIxClient.EXPECT().GetAllWithdrawalsByAddress(gomock.Any()).Return([]api_mods.WithdrawalItem{ - // { - // TransactionHash: "0x123", - // Amount: "1", - // }, - // }, nil).AnyTimes() + ts.TestIxClient.EXPECT().GetAllWithdrawalsByAddress(gomock.Any()).Return([]api_mods.WithdrawalItem{ + { + TransactionHash: "0x123", + Amount: "1", + }, + }, nil).AnyTimes() // Finalize the withdrawal - // finalizeReceipt := op_e2e.FinalizeWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Cfg.Secrets.Alice, proveReceipt, proveParams) - - // // Wait for Pessimism to process the finalized withdrawal and send a notification to the mocked Slack server. - // require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { - // id := ids[0].PathID - // height, err := ts.Subsystems.PathHeight(id) - // if err != nil { - // return false, err - // } - - // return height.Uint64() > finalizeReceipt.BlockNumber.Uint64(), nil - // })) - - // alerts = ts.TestSlackSvr.SlackAlerts() - // require.Equal(t, 3, len(alerts), "expected 3 alerts") - // assert.Contains(t, alerts[0].Text, "unsafe_withdrawal", "expected alert to be for unsafe_withdrawal") - // assert.Contains(t, alerts[0].Text, fakeAddr.String(), "expected alert to be for dummy L2ToL1MessagePasser") - // assert.Contains(t, alerts[0].Text, alertMsg, "expected alert to have alert message") + finalizeReceipt := op_e2e.FinalizeWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Cfg.Secrets.Alice, proveReceipt, params) + + // Wait for Pessimism to process the finalized withdrawal and send a notification to the mocked Slack server. + require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { + id := ids[0].PathID + height, err := ts.Subsystems.PathHeight(id) + if err != nil { + return false, err + } + + return height.Uint64() > finalizeReceipt.BlockNumber.Uint64(), nil + })) + + alerts = ts.TestSlackSvr.SlackAlerts() + require.Equal(t, 1, len(alerts), "expected 1 alert") + assert.Contains(t, alerts[0].Text, core.WithdrawalSafety.String()) + assert.Contains(t, alerts[0].Text, alertMsg, "expected alert to have alert message") + + // Ensure that specific invariant messages are included in the alert + assert.Contains(t, alerts[0].Text, alertMsg, registry.TooSimilarToMax) } // TestWithdrawalSafetyNoInvariants ... Verify that no alerts are produced in the event @@ -418,7 +455,7 @@ func TestWithdrawalSafetyNoInvariants(t *testing.T) { }, }, nil).AnyTimes() - _, proveReceipt := op_e2e.ProveWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Sys.EthInstances["sequencer"], ts.Cfg.Secrets.Alice, withdrawReceipt) + params, proveReceipt := op_e2e.ProveWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Sys.EthInstances["sequencer"], ts.Cfg.Secrets.Alice, withdrawReceipt) // Wait for Pessimism to process the proven withdrawal and send a notification to the mocked Slack server. require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { @@ -431,9 +468,23 @@ func TestWithdrawalSafetyNoInvariants(t *testing.T) { return height != nil && height.Uint64() > proveReceipt.BlockNumber.Uint64(), nil })) - // Ensure that this withdrawal triggered no alerts + // Finalize the withdrawal + finalizeReceipt := op_e2e.FinalizeWithdrawal(t, *ts.Cfg, ts.L1Client, ts.Cfg.Secrets.Alice, proveReceipt, params) + + // Wait for Pessimism to process the finalized withdrawal and send a notification to the mocked Slack server. + require.NoError(t, wait.For(context.Background(), 500*time.Millisecond, func() (bool, error) { + id := ids[0].PathID + height, err := ts.Subsystems.PathHeight(id) + if err != nil { + return false, err + } + + return height.Uint64() > finalizeReceipt.BlockNumber.Uint64(), nil + })) + + // Ensure that this withdrawal flow triggered no alerts alerts := ts.TestSlackSvr.SlackAlerts() - require.Equal(t, 0, len(alerts), "expected 0 alerts") + require.Equal(t, 0, len(alerts)) } // TestFaultDetector ... Ensures that an alert is produced in the presence of a faulty L2Output root diff --git a/internal/api/models/heuristic.go b/internal/api/models/heuristic.go index d5f92013..233c9e0a 100644 --- a/internal/api/models/heuristic.go +++ b/internal/api/models/heuristic.go @@ -53,7 +53,7 @@ type SessionRequestParams struct { // Params ... Returns the heuristic session params func (hrp *SessionRequestParams) Params() *core.SessionParams { - isp := core.NewSessionParams() + isp := core.NewSessionParams(hrp.NetworkType()) for k, v := range hrp.SessionParams { isp.SetValue(k, v) diff --git a/internal/core/core.go b/internal/core/core.go index c7f4cab2..e854ddd9 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -102,6 +102,7 @@ const ( // SessionParams ... Parameters used to initialize a heuristic session type SessionParams struct { + Net Network params map[string]any } @@ -112,8 +113,9 @@ func (sp *SessionParams) Bytes() []byte { } // NewSessionParams ... Initializes heuristic session params -func NewSessionParams() *SessionParams { +func NewSessionParams(n Network) *SessionParams { isp := &SessionParams{ + Net: n, params: make(map[string]any, 0), } isp.params[NestedArgs] = []any{} diff --git a/internal/core/core_test.go b/internal/core/core_test.go index 69caca8e..20b45b08 100644 --- a/internal/core/core_test.go +++ b/internal/core/core_test.go @@ -49,7 +49,7 @@ func Test_EngineRelay(t *testing.T) { } func Test_SessionParams(t *testing.T) { - isp := core.NewSessionParams() + isp := core.NewSessionParams(core.Layer1) assert.NotNil(t, isp, "SessionParams should not be nil") isp.SetValue("tst", "tst") diff --git a/internal/engine/registry/withdrawal_safety.go b/internal/engine/registry/l1_withdrawal_safety.go similarity index 52% rename from internal/engine/registry/withdrawal_safety.go rename to internal/engine/registry/l1_withdrawal_safety.go index 7db36617..8bba6918 100644 --- a/internal/engine/registry/withdrawal_safety.go +++ b/internal/engine/registry/l1_withdrawal_safety.go @@ -5,17 +5,11 @@ import ( "encoding/json" "fmt" "math/big" - "strconv" - "strings" - "time" - - "github.com/base-org/pessimism/internal/common/math" "github.com/base-org/pessimism/internal/client" "github.com/base-org/pessimism/internal/core" "github.com/base-org/pessimism/internal/engine/heuristic" "github.com/base-org/pessimism/internal/logging" - "github.com/ethereum-optimism/optimism/indexer/api/models" "github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -42,22 +36,41 @@ const WithdrawalSafetyMsg = ` Withdrawal Size: %s ETH ` -type WithdrawMetadata struct { - Hash common.Hash - From common.Address - To common.Address +type WithdrawalMeta struct { + Hash common.Hash + InitTx common.Hash + ProvenTx common.Hash + FinalizedTx common.Hash + From common.Address + To common.Address + Value *big.Int +} + +func MetaFromProven(log types.Log, filter *bindings.OptimismPortalFilterer) (*WithdrawalMeta, error) { + proven, err := filter.ParseWithdrawalProven(log) + if err != nil { + return nil, err + } + + return &WithdrawalMeta{ + FinalizedTx: common.HexToHash("0x0"), + Hash: proven.WithdrawalHash, + From: proven.From, + ProvenTx: log.TxHash, + To: proven.To, + }, nil } -func MetaFromProven(log types.Log, filter *bindings.OptimismPortalFilterer) (*WithdrawMetadata, error) { - provenWithdrawal, err := filter.ParseWithdrawalProven(log) +func MetaFromFinalized(log types.Log, filter *bindings.OptimismPortalFilterer) (*WithdrawalMeta, error) { + final, err := filter.ParseWithdrawalFinalized(log) if err != nil { return nil, err } - return &WithdrawMetadata{ - Hash: provenWithdrawal.WithdrawalHash, - From: provenWithdrawal.From, - To: provenWithdrawal.To, + return &WithdrawalMeta{ + FinalizedTx: log.TxHash, + Hash: final.WithdrawalHash, + ProvenTx: common.HexToHash("0x0"), }, nil } @@ -73,8 +86,8 @@ type WithdrawalSafetyCfg struct { L2ToL1Address string `json:"l2_to_l1_address"` } -// WithdrawalSafetyHeuristic ... Withdrawal safety heuristic implementation -type WithdrawalSafetyHeuristic struct { +// L1WithdrawalSafety ... Withdrawal safety heuristic implementation +type L1WithdrawalSafety struct { ctx context.Context cfg *WithdrawalSafetyCfg ixClient client.IxClient @@ -84,7 +97,7 @@ type WithdrawalSafetyHeuristic struct { l1PortalFilter *bindings.OptimismPortalFilterer l2ToL1MsgPasser *bindings.L2ToL1MessagePasserCaller - heuristic.Heuristic + *L2WithdrawalSafety } // Unmarshal ... Converts a general config to a LargeWithdrawal heuristic config @@ -92,8 +105,8 @@ func (cfg *WithdrawalSafetyCfg) Unmarshal(isp *core.SessionParams) error { return json.Unmarshal(isp.Bytes(), &cfg) } -// NewWithdrawalSafetyHeuristic ... Initializer -func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *WithdrawalSafetyCfg) (heuristic.Heuristic, error) { +// NewL1WithdrawalSafety ... Initializer +func NewL1WithdrawalSafety(ctx context.Context, cfg *WithdrawalSafetyCfg) (heuristic.Heuristic, error) { portalAddr := common.HexToAddress(cfg.L1PortalAddress) l2ToL1Addr := common.HexToAddress(cfg.L2ToL1Address) @@ -114,7 +127,12 @@ func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *WithdrawalSafetyCfg) return nil, err } - return &WithdrawalSafetyHeuristic{ + wsh, err := NewL2WithdrawalSafety(ctx, cfg) + if err != nil { + return nil, err + } + + return &L1WithdrawalSafety{ ctx: ctx, cfg: cfg, @@ -124,12 +142,12 @@ func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *WithdrawalSafetyCfg) ixClient: clients.IxClient, l1Client: clients.L1Client, - Heuristic: heuristic.New(core.Log, core.WithdrawalSafety), + L2WithdrawalSafety: wsh.(*L2WithdrawalSafety), }, nil } // Assess ... -func (wsh *WithdrawalSafetyHeuristic) Assess(e core.Event) (*heuristic.ActivationSet, error) { +func (wsh *L1WithdrawalSafety) Assess(e core.Event) (*heuristic.ActivationSet, error) { // TODO - Support running from withdrawal finalized events as well // 1. Validate input @@ -146,14 +164,13 @@ func (wsh *WithdrawalSafetyHeuristic) Assess(e core.Event) (*heuristic.Activatio return nil, fmt.Errorf(couldNotCastErr, "types.Log") } - var wm *WithdrawMetadata + var wm *WithdrawalMeta switch log.Topics[0] { - // TODO(#178) - Feat - Support WithdrawalProven processing in withdrawal_safety heuristic - // case WithdrawalFinalSig: - // wm, err = MetaFromFinalized(log, wi.l1PortalFilter) - // if err != nil { - // return nil, err - // } + case WithdrawalFinalSig: + // Since the to/from fields are unknown we cannot query + // the indexer API + invs := wsh.VerifyHash(wm.Hash) + return wsh.Execute(invs, wm) case WithdrawalProvenSig: wm, err = MetaFromProven(log, wsh.l1PortalFilter) @@ -191,100 +208,19 @@ func (wsh *WithdrawalSafetyHeuristic) Assess(e core.Event) (*heuristic.Activatio return nil, err } - parsedInt, err := strconv.ParseUint(corrWithdrawal.Amount, 10, 64) - if err != nil { - return nil, err - } - - withdrawalWEI := big.NewInt(0).SetInt64(int64(parsedInt)) + b := []byte(corrWithdrawal.Amount) + withdrawalWEI := big.NewInt(0).SetBytes(b) correlated, err := wsh.l2ToL1MsgPasser.SentMessages(nil, wm.Hash) if err != nil { return nil, err } - invariants := wsh.GetInvariants(&corrWithdrawal, portalWEI, withdrawalWEI, correlated) - - // 5. Process activation set messages from invariant analysis - msgs := make([]string, 0) - - for _, inv := range invariants { - if success, msg := inv(); success { - msgs = append(msgs, msg) - } - } - - if len(msgs) == 0 { - return heuristic.NoActivations(), nil - } + h := common.HexToHash(corrWithdrawal.TransactionHash) + wm.Value = withdrawalWEI - msg := "\n*" + strings.Join(msgs[0:len(msgs)-1], "\n *") - msg += msgs[len(msgs)-1] - return heuristic.NewActivationSet().Add( - &heuristic.Activation{ - TimeStamp: time.Now(), - Message: fmt.Sprintf(WithdrawalSafetyMsg, msg, wsh.cfg.L1PortalAddress, wsh.cfg.L2ToL1Address, - wsh.ID(), log.TxHash.String(), corrWithdrawal.TransactionHash, math.WeiToEther(withdrawalWEI).String()), - }, - ), nil -} + invs := wsh.GetInvariants(portalWEI, withdrawalWEI, correlated) + invs = append(invs, wsh.VerifyHash(h)...) -// GetInvariants ... Returns a list of invariants to be checked for in the assessment -func (wsh *WithdrawalSafetyHeuristic) GetInvariants(corrWithdrawal *models.WithdrawalItem, - portalWEI, withdrawalWEI *big.Int, correlated bool) []func() (bool, string) { - maxAddr := common.HexToAddress("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") - minAddr := common.HexToAddress("0x0000000000000000000000000000000000000000") - - portalAmt := new(big.Float).SetInt(portalWEI) - withdrawAmt := new(big.Float).SetInt(withdrawalWEI) - - // Run the following invariant functions in order - return []func() (bool, string){ - // A - // Check if the proven withdrawal amount is greater than the OptimismPortal value - func() (bool, string) { - return withdrawalWEI.Cmp(portalWEI) >= 0, GreaterThanPortal - }, - // B - // Check if the proven withdrawal amount is greater than threshold % of the OptimismPortal value - func() (bool, string) { - return math.PercentOf(withdrawAmt, portalAmt).Cmp(big.NewFloat(wsh.cfg.Threshold*100)) == 1, - fmt.Sprintf(GreaterThanThreshold, wsh.cfg.Threshold) - }, - // C - // Ensure the proven withdrawal exists in the L2ToL1MessagePasser storage - func() (bool, string) { - return !correlated, UncorrelatedWithdraw - }, - // D - // Ensure message_hash != 0x0...0 and message_hash != 0xf...f - func() (bool, string) { - if corrWithdrawal.MessageHash == minAddr.String() { - return true, TooSimilarToZero - } - - if corrWithdrawal.MessageHash == maxAddr.String() { - return true, TooSimilarToMax - } - - return false, "" - }, - // E - // Ensure that message isn't super similar to erroneous values using Sorenson-Dice coefficient - func() (bool, string) { - c0 := math.SorensonDice(corrWithdrawal.MessageHash, minAddr.String()) - c1 := math.SorensonDice(corrWithdrawal.MessageHash, maxAddr.String()) - threshold := wsh.cfg.CoefficientThreshold - - if c0 >= threshold { - return true, TooSimilarToZero - } - - if c1 >= threshold { - return true, TooSimilarToMax - } - - return false, "" - }, - } + return wsh.Execute(invs, wm) } diff --git a/internal/engine/registry/l2_withdrawal_safety.go b/internal/engine/registry/l2_withdrawal_safety.go new file mode 100644 index 00000000..13fded56 --- /dev/null +++ b/internal/engine/registry/l2_withdrawal_safety.go @@ -0,0 +1,223 @@ +package registry + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/base-org/pessimism/internal/common/math" + + "github.com/base-org/pessimism/internal/client" + "github.com/base-org/pessimism/internal/core" + "github.com/base-org/pessimism/internal/engine/heuristic" + "github.com/base-org/pessimism/internal/logging" + "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "go.uber.org/zap" +) + +// L2WithdrawalSafety ... Withdrawal safety heuristic implementation +type L2WithdrawalSafety struct { + ctx context.Context + cfg *WithdrawalSafetyCfg + ixClient client.IxClient + + l1Client client.EthClient + // NOTE - These values can be ingested from the chain config in the future + l1PortalFilter *bindings.OptimismPortalFilterer + l2ToL1MsgPasser *bindings.L2ToL1MessagePasserCaller + l2ToL1Filter *bindings.L2ToL1MessagePasserFilterer + + heuristic.Heuristic +} + +// NewL2WithdrawalSafety ... Initializer +func NewL2WithdrawalSafety(ctx context.Context, cfg *WithdrawalSafetyCfg) (heuristic.Heuristic, error) { + portalAddr := common.HexToAddress(cfg.L1PortalAddress) + l2ToL1Addr := common.HexToAddress(cfg.L2ToL1Address) + + clients, err := client.FromContext(ctx) + if err != nil { + return nil, err + } + + // NOTE - All OP Stack op bindings could be moved to a single object for reuse + // across heuristics + filter, err := bindings.NewOptimismPortalFilterer(portalAddr, clients.L1Client) + if err != nil { + return nil, err + } + + l2ToL1MsgPasser, err := bindings.NewL2ToL1MessagePasserCaller(l2ToL1Addr, clients.L2Client) + if err != nil { + return nil, err + } + + l2ToL1Filter, err := bindings.NewL2ToL1MessagePasserFilterer(l2ToL1Addr, clients.L2Client) + if err != nil { + return nil, err + } + + return &L2WithdrawalSafety{ + ctx: ctx, + cfg: cfg, + + l1PortalFilter: filter, + l2ToL1MsgPasser: l2ToL1MsgPasser, + l2ToL1Filter: l2ToL1Filter, + + ixClient: clients.IxClient, + l1Client: clients.L1Client, + + Heuristic: heuristic.New(core.Log, core.WithdrawalSafety), + }, nil +} + +// Assess ... +func (wsh *L2WithdrawalSafety) Assess(td core.Event) (*heuristic.ActivationSet, error) { + // TODO - Support running from withdrawal finalized events as well + + // 1. Validate input + logging.NoContext().Debug("Checking activation for withdrawal safety heuristic", + zap.String("data", fmt.Sprintf("%v", td))) + + err := wsh.Validate(td) + if err != nil { + return nil, err + } + + log, success := td.Value.(types.Log) + if !success { + return nil, fmt.Errorf(couldNotCastErr, "types.Log") + } + + msgPassed, err := wsh.l2ToL1Filter.ParseMessagePassed(log) + if err != nil { + return nil, err + } + + // 4. Fetch the OptimismPortal balance at the L1 block height which the withdrawal was proven + portalWEI, err := wsh.l1Client.BalanceAt(context.Background(), common.HexToAddress(wsh.cfg.L1PortalAddress), nil) + if err != nil { + return nil, err + } + + b := msgPassed.WithdrawalHash[0:len(msgPassed.WithdrawalHash)] + + invs := wsh.GetInvariants(portalWEI, msgPassed.Value, true) + invs = append(invs, wsh.VerifyHash(common.BytesToHash(b))...) + + // 5. Process activation set messages from invariant analysis + msgs := make([]string, 0) + + for _, inv := range invs { + if success, msg := inv(); success { + msgs = append(msgs, msg) + } + } + + if len(msgs) == 0 { + return heuristic.NoActivations(), nil + } + + msg := "\n*" + strings.Join(msgs[0:len(msgs)-1], "\n *") + msg += msgs[len(msgs)-1] + return heuristic.NewActivationSet().Add( + &heuristic.Activation{ + TimeStamp: time.Now(), + Message: fmt.Sprintf(WithdrawalSafetyMsg, msg, wsh.cfg.L1PortalAddress, wsh.cfg.L2ToL1Address, + wsh.ID(), "N/A", log.TxHash.String(), math.WeiToEther(msgPassed.Value).String()), + }, + ), nil +} + +// GetInvariants ... Returns a list of invariants to be checked for in the assessment +func (wsh *L2WithdrawalSafety) GetInvariants(portalWEI, withdrawalWEI *big.Int, correlated bool) []Invariant { + portalAmt := new(big.Float).SetInt(portalWEI) + withdrawAmt := new(big.Float).SetInt(withdrawalWEI) + + // Run the following invariant functions in order + return []Invariant{ + // A + // Check if the proven withdrawal amount is greater than the OptimismPortal value + func() (bool, string) { + return withdrawalWEI.Cmp(portalWEI) >= 0, GreaterThanPortal + }, + // B + // Check if the proven withdrawal amount is greater than threshold % of the OptimismPortal value + func() (bool, string) { + return math.PercentOf(withdrawAmt, portalAmt).Cmp(big.NewFloat(wsh.cfg.Threshold*100)) == 1, + fmt.Sprintf(GreaterThanThreshold, wsh.cfg.Threshold) + }, + // C + // Ensure the proven withdrawal exists in the L2ToL1MessagePasser storage + func() (bool, string) { + return !correlated, UncorrelatedWithdraw + }, + } +} + +func (wsh *L2WithdrawalSafety) VerifyHash(hash common.Hash) []Invariant { + maxAddr := common.HexToAddress("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + minAddr := common.HexToAddress("0x0000000000000000000000000000000000000000") + + return []Invariant{ + // Ensure message_hash != 0x0...0 and message_hash != 0xf...f + func() (bool, string) { + if hash.String() == minAddr.String() { + return true, TooSimilarToZero + } + + if hash.String() == maxAddr.String() { + return true, TooSimilarToMax + } + + return false, "" + }, + // Ensure that message isn't super similar to erroneous values using Sorenson-Dice coefficient + func() (bool, string) { + c0 := math.SorensonDice(hash.String(), minAddr.String()) + c1 := math.SorensonDice(hash.String(), maxAddr.String()) + threshold := wsh.cfg.CoefficientThreshold + + if c0 >= threshold { + return true, TooSimilarToZero + } + + if c1 >= threshold { + return true, TooSimilarToMax + } + + return false, "" + }, + } +} + +func (wsh *L2WithdrawalSafety) Execute(invs []Invariant, meta *WithdrawalMeta) (*heuristic.ActivationSet, error) { + // 5. Process activation set messages from invariant analysis + msgs := make([]string, 0) + + for _, inv := range invs { + if success, msg := inv(); success { + msgs = append(msgs, msg) + } + } + + if len(msgs) == 0 { + return heuristic.NoActivations(), nil + } + + msg := "\n*" + strings.Join(msgs[0:len(msgs)-1], "\n *") + msg += msgs[len(msgs)-1] + return heuristic.NewActivationSet().Add( + &heuristic.Activation{ + TimeStamp: time.Now(), + Message: fmt.Sprintf(WithdrawalSafetyMsg, msg, wsh.cfg.L1PortalAddress, wsh.cfg.L2ToL1Address, + wsh.ID(), meta.ProvenTx.String(), meta.InitTx.String(), math.WeiToEther(meta.Value).String()), + }, + ), nil +} diff --git a/internal/engine/registry/registry.go b/internal/engine/registry/registry.go index 9b5bcdce..f17ed0fb 100644 --- a/internal/engine/registry/registry.go +++ b/internal/engine/registry/registry.go @@ -11,19 +11,20 @@ import ( ) // HeuristicTable ... Heuristic table -type HeuristicTable map[core.HeuristicType]*InvRegister +type HeuristicTable map[core.HeuristicType]*Registry -// InvRegister ... Heuristic register struct -type InvRegister struct { +type Registry struct { PrepareValidate func(*core.SessionParams) error Policy core.ChainSubscription InputType core.TopicType Constructor func(ctx context.Context, isp *core.SessionParams) (heuristic.Heuristic, error) } +type Invariant func() (bool, string) + // NewHeuristicTable ... Initializer func NewHeuristicTable() HeuristicTable { - tbl := map[core.HeuristicType]*InvRegister{ + tbl := map[core.HeuristicType]*Registry{ core.BalanceEnforcement: { PrepareValidate: ValidateAddressing, Policy: core.BothNetworks, @@ -89,7 +90,7 @@ func constructFaultDetector(ctx context.Context, isp *core.SessionParams) (heuri return NewFaultDetector(ctx, cfg) } -// constructWithdrawalSafety ... Constructs a large withdrawal heuristic instance +// constructWithdrawalSafety ... Constructs a withdrawal safety heuristic instance func constructWithdrawalSafety(ctx context.Context, isp *core.SessionParams) (heuristic.Heuristic, error) { cfg := &WithdrawalSafetyCfg{} err := cfg.Unmarshal(isp) @@ -107,7 +108,16 @@ func constructWithdrawalSafety(ctx context.Context, isp *core.SessionParams) (he return nil, fmt.Errorf("invalid coefficient threshold supplied for withdrawal safety heuristic") } - return NewWithdrawalSafetyHeuristic(ctx, cfg) + switch isp.Net { + case core.Layer1: + return NewL1WithdrawalSafety(ctx, cfg) + + case core.Layer2: + return NewL2WithdrawalSafety(ctx, cfg) + + default: + return nil, fmt.Errorf("invalid network supplied for withdrawal safety heuristic") + } } // ValidateTracking ... Ensures that an address and nested args exist in the session params @@ -156,23 +166,26 @@ func WithdrawHeuristicPrep(cfg *core.SessionParams) error { return err } - _, err = cfg.Value(core.L2ToL1MessagePasser) + l2MsgPasser, err := cfg.Value(core.L2ToL1MessagePasser) if err != nil { return err } - // Configure the session to inform the ETL to subscribe - // to withdrawal proof events from the L1Portal contract - cfg.SetValue(logging.AddrKey, l1Portal) - err = ValidateNoTopicsExist(cfg) if err != nil { return err } - cfg.SetNestedArg(WithdrawalProvenEvent) - // TODO(#178) - Feat - Support WithdrawalProven processing in withdrawal_safety heuristic - // cfg.SetNestedArg(WithdrawalFinalEvent) + switch cfg.Net { + case core.Layer1: + cfg.SetValue(logging.AddrKey, l1Portal) + cfg.SetNestedArg(WithdrawalProvenEvent) + // cfg.SetNestedArg(WithdrawalFinalEvent) + case core.Layer2: + cfg.SetValue(logging.AddrKey, l2MsgPasser) + cfg.SetNestedArg(MessagePassed) + } + return nil } diff --git a/internal/engine/registry/registry_test.go b/internal/engine/registry/registry_test.go index ff384fab..0e9fe5ed 100644 --- a/internal/engine/registry/registry_test.go +++ b/internal/engine/registry/registry_test.go @@ -10,7 +10,7 @@ import ( ) func Test_AddressPreprocess(t *testing.T) { - isp := core.NewSessionParams() + isp := core.NewSessionParams(core.Layer2) err := registry.ValidateAddressing(isp) assert.Error(t, err, "failure should occur when no address is provided") @@ -21,7 +21,7 @@ func Test_AddressPreprocess(t *testing.T) { } func Test_EventPreprocess(t *testing.T) { - isp := core.NewSessionParams() + isp := core.NewSessionParams(core.Layer1) err := registry.ValidateTracking(isp) assert.Error(t, err, "failure should occur when no address is provided") @@ -35,7 +35,7 @@ func Test_EventPreprocess(t *testing.T) { } func TestUnsafeWithdrawPrepare(t *testing.T) { - isp := core.NewSessionParams() + isp := core.NewSessionParams(core.Layer1) err := registry.WithdrawHeuristicPrep(isp) assert.Error(t, err, "failure should occur when no l1_portal is provided")