Skip to content
This repository has been archived by the owner on May 29, 2024. It is now read-only.

Commit

Permalink
[epociask/unsafe-withdrawal-heuristic] Passing tests and lint
Browse files Browse the repository at this point in the history
  • Loading branch information
Ethen Pociask committed Oct 27, 2023
1 parent 20fa54f commit 26cdb3e
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 39 deletions.
13 changes: 7 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ linters-settings:
# Default: true
skipRecvDeref: false

gomnd:
# List of function regex patterns to exclude from analysis.
# Default: []
ignored-functions:
-
# gomnd:
# # List of function regex patterns to exclude from analysis.
# # Default: []
# ignored-functions:
# -
gomodguard:
blocked:
# List of blocked modules.
Expand Down Expand Up @@ -144,7 +144,7 @@ linters:
- gocritic # provides diagnostics that check for bugs, performance and style issues
- gocyclo # computes and checks the cyclomatic complexity of functions
- goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt
- gomnd # detects magic numbers
# - gomnd # detects magic numbers
- gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations
- goprintffuncname # checks that printf-like functions are named with f at the end
- gosec # inspects source code for security problems
Expand Down Expand Up @@ -207,6 +207,7 @@ issues:
linters:
- gocritic
- unparam

- path: "_test\\.go"
linters:
- gocognit
Expand Down
29 changes: 29 additions & 0 deletions internal/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,32 @@ func Test_DLQ(t *testing.T) {
assert.Equal(t, len(entries), 4)
assert.True(t, dlq.Empty(), true)
}

// Test_SorensonDice ... Tests Sorenson Dice similarity
func Test_SorensonDice(t *testing.T) {
var tests = []struct {
name string
function func(t *testing.T, a, b string, expected float64)
}{
{
name: "Equal Strings",
function: func(t *testing.T, a, b string, expected float64) {
assert.Equal(t, common.SorensonDice(a, b), expected)
},
},
{
name: "Unequal Strings",
function: func(t *testing.T, a, b string, expected float64) {
assert.Equal(t, common.SorensonDice(a, b), expected)
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.function(t, "0x123", "0x123", 1)
test.function(t, "0x123", "0x124", 0.75)
})
}

}
35 changes: 32 additions & 3 deletions internal/common/math.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,38 @@ package common

import "math/big"

// PercentOf - calculate what percent [number1] is of [number2].
// ex. 300 is 12.5% of 2400
// PercentOf - calculate what percent x0 is of x1.
func PercentOf(part, total *big.Float) *big.Float {
x0 := new(big.Float).Mul(part, big.NewFloat(100))
whole := 100.0
x0 := new(big.Float).Mul(part, big.NewFloat(whole))
return new(big.Float).Quo(x0, total)
}

// SorensonDice - calculate the Sorenson-Dice coefficient between two strings.
func SorensonDice(s1, s2 string) float64 {
// https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient
// 2 * |X & Y| / (|X| + |Y|)

var (
n1 = len(s1)
n2 = len(s2)
)

if n1 == 0 || n2 == 0 {
return 0
}

intersects := 0
for i := 0; i < n1-1; i++ {
a := s1[i : i+2]
for j := 0; j < n2-1; j++ {
b := s2[j : j+2]
if a == b {
intersects++
break
}
}
}

return float64(2*intersects) / float64(n1+n2-2)
}
4 changes: 2 additions & 2 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ func (hce *hardCodedEngine) EventLoop(ctx context.Context) {
var actSet *heuristic.ActivationSet

retryStrategy := &retry.ExponentialStrategy{Min: 1000, Max: 20_000, MaxJitter: 250}
if _, err := retry.Do[interface{}](ctx, 10, retryStrategy, func() (interface{}, error) {
if _, err := retry.Do[any](ctx, 10, retryStrategy, func() (any, error) {
actSet = hce.Execute(ctx, execInput.hi.Input, execInput.h)
metrics.WithContext(ctx).RecordHeuristicRun(execInput.h)
metrics.WithContext(ctx).RecordInvExecutionTime(execInput.h, float64(time.Since(start).Nanoseconds()))
// a-ok!
return nil, nil
return 0, nil
}); err != nil {
logger.Error("Failed to execute heuristic", zap.Error(err))
metrics.WithContext(ctx).RecordAssessmentError(execInput.h)
Expand Down
4 changes: 2 additions & 2 deletions internal/engine/heuristic/heuristic.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ func (bi *BaseHeuristic) InputType() core.RegisterType {
}

// Assess ... Determines if a heuristic activation has occurred; defaults to no-op
func (bi *BaseHeuristic) Assess(td core.TransitData) (*ActivationSet, error) {
return nil, nil
func (bi *BaseHeuristic) Assess(_ core.TransitData) (*ActivationSet, error) {
return NoActivations(), nil
}

// SetSUUID ... Sets the heuristic session UUID
Expand Down
6 changes: 1 addition & 5 deletions internal/engine/registry/fault_detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/ethereum/go-ethereum/rlp"

"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -59,8 +58,7 @@ func blockToInfo(b *types.Block) blockInfo {

// faultDetectorInv ... faultDetectorInv implementation
type faultDetectorInv struct {
eventHash common.Hash
cfg *FaultDetectorCfg
cfg *FaultDetectorCfg

l2tol1MessagePasser common.Address
l2OutputOracleFilter *bindings.L2OutputOracleFilterer
Expand All @@ -79,7 +77,6 @@ func NewFaultDetector(ctx context.Context, cfg *FaultDetectorCfg) (heuristic.Heu
return nil, err
}

outputSig := crypto.Keccak256Hash([]byte(OutputProposedEvent))
addr := common.HexToAddress(cfg.L2ToL1Address)

outputOracle, err := bindings.NewL2OutputOracleFilterer(addr, bundle.L1Client)
Expand All @@ -90,7 +87,6 @@ func NewFaultDetector(ctx context.Context, cfg *FaultDetectorCfg) (heuristic.Heu
return &faultDetectorInv{
cfg: cfg,

eventHash: outputSig,
l2OutputOracleFilter: outputOracle,
l2tol1MessagePasser: addr,
stats: metrics.WithContext(ctx),
Expand Down
76 changes: 55 additions & 21 deletions internal/engine/registry/withdrawal_safety.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
)

const (
largeWithdrawal = "Large withdrawal has been proven on L1"
greaterThanThreshold = "A withdraw was proven that is >= %f of the Optimism Portal balance"
uncorrelatedWithdraw = "Withdrawal proven on L1 does not exist in L2ToL1MessagePasser storage"
greaterThanPortal = "Withdrawal amount is greater than the Optimism Portal balance"
)
Expand All @@ -38,6 +38,7 @@ const unsafeWithdrawalMsg = `

// UnsafeWithdrawalCfg ... Configuration for the balance heuristic
type UnsafeWithdrawalCfg struct {
// % of OptimismPortal balance that is considered a large withdrawal
Threshold float64 `json:"threshold"`

L1PortalAddress string `json:"l1_portal_address"`
Expand All @@ -64,7 +65,6 @@ func (cfg *UnsafeWithdrawalCfg) Unmarshal(isp *core.SessionParams) error {

// NewWithdrawalSafetyHeuristic ... Initializer
func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *UnsafeWithdrawalCfg) (heuristic.Heuristic, error) {

// Validate that threshold is on exclusive range (0, 100)
if cfg.Threshold >= 100 || cfg.Threshold < 0 {
return nil, fmt.Errorf("invalid threshold supplied for withdrawal safety heuristic")
Expand All @@ -84,6 +84,9 @@ func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *UnsafeWithdrawalCfg)
}

l2ToL1MsgPasser, err := bindings.NewL2ToL1MessagePasserCaller(l2ToL1Addr, clients.L2Client)
if err != nil {
return nil, err
}

return &WithdrawalSafetyHeuristic{
ctx: ctx,
Expand All @@ -101,20 +104,15 @@ func NewWithdrawalSafetyHeuristic(ctx context.Context, cfg *UnsafeWithdrawalCfg)

// Assess ... Verifies than an L1 WithdrawalProven has a correlating hash
// to the withdrawal storage of the L2ToL1MessagePasser
// TODO - Segment this into composite functions
func (wi *WithdrawalSafetyHeuristic) Assess(td core.TransitData) (*heuristic.ActivationSet, error) {
// TODO - Support running from withdrawal initiated or withdrawal proven events

clients, err := client.FromContext(wi.ctx)
if err != nil {
return nil, err
}

logging.NoContext().Debug("Checking activation for withdrawal enforcement heuristic",
zap.String("data", fmt.Sprintf("%v", td)))

// 1. Validate and extract data input
if td.Type != wi.InputType() {
return nil, fmt.Errorf("invalid type supplied")
if wi.ValidateInput(td) != nil {
return nil, fmt.Errorf("invalid input supplied")
}

if td.Address.String() != wi.cfg.L1PortalAddress {
Expand All @@ -133,7 +131,7 @@ func (wi *WithdrawalSafetyHeuristic) Assess(td core.TransitData) (*heuristic.Act
}

// 3. Get withdrawal metadata from OP Indexer API
withdrawals, err := clients.IndexerClient.GetAllWithdrawalsByAddress(provenWithdrawal.From)
withdrawals, err := wi.indexerClient.GetAllWithdrawalsByAddress(provenWithdrawal.From)
if err != nil {
return nil, err
}
Expand All @@ -145,41 +143,77 @@ func (wi *WithdrawalSafetyHeuristic) Assess(td core.TransitData) (*heuristic.Act
// TODO - Update withdrawal decoding to convert to big.Int instead of string
corrWithdrawal := withdrawals[0]

// 4. Fetch the OptimismPortal balance at the time which the withdrawal was proven
portalWEI, err := wi.l1Client.BalanceAt(context.Background(), common.HexToAddress(wi.cfg.L1PortalAddress),
big.NewInt(int64(log.BlockNumber)))
if err != nil {
return nil, err
}

portalETH := p_common.WeiToEther(portalWEI)

withdrawalWEI := big.NewInt(0).SetBytes([]byte(corrWithdrawal.Amount))
withdrawalETH := p_common.WeiToEther(withdrawalWEI)

correlated, err := wi.l2ToL1MsgPasser.SentMessages(nil, provenWithdrawal.WithdrawalHash)
if err != nil {
return nil, err
}

// 4. Perform invariant analysis using the withdrawal metadata
maxAddr := common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff")
minAddr := common.HexToAddress("0x0")

portalETH := p_common.WeiToEther(portalWEI)
withdrawalETH := p_common.WeiToEther(withdrawalWEI)

// 5. Perform invariant analysis using the fetched withdrawal metadata
invariants := []func() (bool, string){
// 4.1
// 5.1
// Check if the proven withdrawal amount is greater than the OptimismPortal value
func() (bool, string) {
return withdrawalWEI.Cmp(portalWEI) >= 0, greaterThanPortal
},
// 4.2
// Check if the proven withdrawal amount is greater than 5% of the OptimismPortal value
// 5.2
// Check if the proven withdrawal amount is greater than threshold % of the OptimismPortal value
func() (bool, string) {
return p_common.PercentOf(withdrawalETH, portalETH).Cmp(big.NewFloat(5)) == 1, `
A withdraw was proven that is >= 5% of the Optimism Portal balance`
return p_common.PercentOf(withdrawalETH, portalETH).Cmp(big.NewFloat(wi.cfg.Threshold)) == 1,
fmt.Sprintf(greaterThanThreshold, wi.cfg.Threshold)
},
// 4.3
// 5.3
// Ensure the proven withdrawal exists in the L2ToL1MessagePasser storage
func() (bool, string) {
return !correlated, uncorrelatedWithdraw
},
// 5.4
// Ensure message_hash != 0x0 (0x0 is the default value for bytes32)
func() (bool, string) {
if corrWithdrawal.MessageHash == minAddr.String() {
return true, "Withdrawal message hash is 0x0000000000000000000000000000000000000000"
}

if corrWithdrawal.MessageHash == maxAddr.String() {
return true, "Withdrawal message hash is 0xffffffffffffffffffffffffffffffffffffffff"
}

return false, ""
},
// 5.5
// Ensure that zero address and max address aren't super similar to Sorenson-Dice coefficient
func() (bool, string) {
c0 := p_common.SorensonDice(corrWithdrawal.MessageHash, minAddr.String())
c1 := p_common.SorensonDice(corrWithdrawal.MessageHash, maxAddr.String())
threshold := 0.95

if c0 >= threshold {
return true, "Zero address is too similar to message hash"
}

if c1 >= threshold {
return true, "Max address is too similar to message hash"
}

return false, ""
},
}

// 6. Process activation set from invariant analysis
as := heuristic.NewActivationSet()
for _, inv := range invariants {
if success, msg := inv(); success {
Expand Down

0 comments on commit 26cdb3e

Please sign in to comment.