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

Withdrawal Enforcement Invariant Implementation #93

Merged
merged 14 commits into from
Jun 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ GREEN = \033[0;32m
BLUE = \033[0;34m
COLOR_END = \033[0;39m

TEST_LIMIT = 30s
TEST_LIMIT = 120s

build-app:
@echo "$(BLUE)» building application binary... $(COLOR_END)"
Expand All @@ -31,7 +31,7 @@ test:

.PHONY: test-e2e
e2e-test:
@ go test ./e2e/... -timeout $(TEST_LIMIT)
@ go test ./e2e/... -timeout $(TEST_LIMIT)

.PHONY: lint
lint:
Expand Down
8 changes: 3 additions & 5 deletions cmd/pessimism/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func main() {

ctx = app.InitializeContext(ctx, ss, l1Client, l2Client)

pessimism, shutDown, err := app.NewPessimismApp(ctx, cfg)
pessimism, shutdown, err := app.NewPessimismApp(ctx, cfg)

if err != nil {
logger.Fatal("Error creating pessimism application", zap.Error(err))
Expand All @@ -67,10 +67,8 @@ func main() {
logger.Debug("Application state successfully bootstrapped")
}

pessimism.ListenForShutdown(shutDown)

logger.Debug("Waiting for all application threads to end")

pessimism.ListenForShutdown(shutdown)
logger.Info("Successful pessimism shutdown")

os.Exit(0)
}
36 changes: 33 additions & 3 deletions docs/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
## Balance Enforcement
The hardcoded `balance_enforcement` invariant checks the native ETH balance of some address every `n` milliseconds and alerts to slack if the account's balance is ever less than `lower` or greater than `upper` value. This invariant is useful for monitoring hot wallets and other accounts that should always have a balance above a certain threshold.


### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| address | string | The address to check the balance of |
| lower | float | The ETH lower bound of the balance |
| upper | float | The ETH upper bound of the balance |


### Example Deploy Request
```
curl --location --request POST 'http://localhost:8080/v0/invariant' \
Expand Down Expand Up @@ -63,4 +61,36 @@ curl --location --request POST 'http://localhost:8080/v0/invariant' \
"args": ["Transfer(address,address,uint256)"]
}
}
}'
}'
```

## Withdrawal Enforcement
**NOTE:** This invariant requires an active RPC connection to both L1 and L2 networks.

The hardcoded `withdrawal_enforcement` invariant scans for active `WithdrawalProven` events on an L1Portal contract. Once an event is detected, the invariant proceeds to scan for the corresponding `withdrawlHash` event on the L2ToL1MesagePasser contract's internal state. If the `withdrawlHash` is not found, the invariant alerts to slack.

### Parameters
| Name | Type | Description |
| ---- | ---- | ----------- |
| l1_portal | string | The address of the L1Portal contract |
| l2_messager | string | The address of the L2ToL1MessagePasser contract |


### Example Deploy Request
```
curl --location --request POST 'http://localhost:8080/v0/invariant' \
--header 'Content-Type: text/plain' \
--data-raw '{
"method": "run",
"params": {
"network": "layer1",
"pipeline_type": "live",
"type": "withdrawal_enforcement",
"start_height": null,
"alert_destination": "slack",
"invariant_params": {
"l1_portal": "0x111",
"l2_messager": "0x333",
},
}'
```
7 changes: 5 additions & 2 deletions e2e/e2e.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,17 @@ func CreateSysTestSuite(t *testing.T) *SysTestSuite {
ctx := context.Background()

cfg := op_e2e.DefaultSystemConfig(t)
cfg.DeployConfig.FinalizationPeriodSeconds = 6

sys, err := cfg.Start()
if err != nil {
t.Fatal(err)
}

ss := state.NewMemState()

ctx = app.InitializeContext(ctx, ss, sys.Clients["l1"], sys.Clients["sequencer"])
ctx = app.InitializeContext(ctx, ss,
sys.Clients["l1"],
sys.Clients["sequencer"])

appCfg := DefaultTestConfig()

Expand Down
169 changes: 169 additions & 0 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package e2e_test

import (
"context"
"crypto/ecdsa"
"math/big"

"github.com/ethereum/go-ethereum/rpc"

"testing"
"time"

"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"

"github.com/ethereum-optimism/optimism/op-node/withdrawals"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/stretchr/testify/assert"

"github.com/base-org/pessimism/e2e"
Expand Down Expand Up @@ -174,3 +184,162 @@ func Test_Contract_Event(t *testing.T) {
assert.Equal(t, len(posts), 1, "No system contract event alert was sent")
assert.Contains(t, posts[0].Text, "contract_event", "System contract event alert was not sent")
}

// TestAccount defines an account generated b
type TestAccount struct {
Key *ecdsa.PrivateKey
Addr common.Address
L1Opts *bind.TransactOpts
L2Opts *bind.TransactOpts
}

// Test_Withdrawal_Enforcement ...
func Test_Withdrawal_Enforcement(t *testing.T) {

ts := e2e.CreateSysTestSuite(t)
ts.Cfg.DeployConfig.FinalizationPeriodSeconds = 6
defer ts.Close()

// Obtain our sequencer, verifier, and transactor keypair.
l1Client := ts.Sys.Clients["l1"]
l2Seq := ts.Sys.Clients["sequencer"]
l2Verif := ts.Sys.Clients["verifier"]

// Define our L1 transaction timeout duration.
txTimeoutDuration := 10 * time.Duration(ts.Cfg.DeployConfig.L1BlockTime) * time.Second

// Bind to the deposit contract.
depositContract, err := bindings.NewOptimismPortal(predeploys.DevOptimismPortalAddr, l1Client)
_ = depositContract
assert.NoError(t, err)

// Create a test account state for our transactor.
transactorKey := ts.Cfg.Secrets.Alice
transactor := TestAccount{
Key: transactorKey,
L1Opts: nil,
L2Opts: nil,
}

transactor.L1Opts, err = bind.NewKeyedTransactorWithChainID(transactor.Key, ts.Cfg.L1ChainIDBig())
assert.NoError(t, err)
transactor.L2Opts, err = bind.NewKeyedTransactorWithChainID(transactor.Key, ts.Cfg.L2ChainIDBig())
assert.NoError(t, err)

// Bind to the L2-L1 message passer.
l2l1MessagePasser, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, l2Seq)
assert.NoError(t, err, "error binding to message passer on L2")

// Deploy a dummy L2ToL1 message passer for testing.
fakeAddr, tx, _, err := bindings.DeployL2ToL1MessagePasser(transactor.L2Opts, l2Seq)
assert.NoError(t, err, "error deploying message passer on L2")

_, err = e2e.WaitForTransaction(tx.Hash(), l2Seq, txTimeoutDuration)
assert.NoError(t, err, "error waiting for transaction")

// Determine the address our request will come from.
fromAddr := crypto.PubkeyToAddress(transactor.Key.PublicKey)

// Initiate Withdrawal.
withdrawAmount := big.NewInt(500_000_000_000)
transactor.L2Opts.Value = withdrawAmount
tx, err = l2l1MessagePasser.InitiateWithdrawal(transactor.L2Opts, fromAddr, big.NewInt(21000), nil)
assert.Nil(t, err, "sending initiate withdraw tx")

// Wait for the transaction to appear in the L2 verifier.
receipt, err := e2e.WaitForTransaction(tx.Hash(), l2Verif, txTimeoutDuration)
assert.Nil(t, err, "withdrawal initiated on L2 sequencer")
assert.Equal(t, receipt.Status, types.ReceiptStatusSuccessful, "transaction failed")

// Wait for the finalization period, then we can finalize this withdrawal.
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Duration(ts.Cfg.DeployConfig.L1BlockTime)*time.Second)
blockNumber, err := withdrawals.WaitForFinalizationPeriod(ctx, l1Client, predeploys.DevOptimismPortalAddr, receipt.BlockNumber)
cancel()
assert.Nil(t, err)

ctx, cancel = context.WithTimeout(context.Background(), txTimeoutDuration)
header, err := l2Verif.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber))
cancel()
assert.Nil(t, err)

l2OutputOracle, err := bindings.NewL2OutputOracleCaller(predeploys.DevL2OutputOracleAddr, l1Client)
assert.Nil(t, err)

rpcClient, err := rpc.Dial(ts.Sys.Nodes["verifier"].WSEndpoint())

assert.Nil(t, err)
proofCl := gethclient.New(rpcClient)
receiptCl := ethclient.NewClient(rpcClient)

// Now create the withdrawal
params, err := withdrawals.ProveWithdrawalParameters(context.Background(), proofCl, receiptCl, tx.Hash(), header, l2OutputOracle)
assert.Nil(t, err)

// Obtain our withdrawal parameters
withdrawalTransaction := &bindings.TypesWithdrawalTransaction{
Nonce: params.Nonce,
Sender: params.Sender,
Target: params.Target,
Value: params.Value,
GasLimit: params.GasLimit,
Data: params.Data,
}
l2OutputIndexParam := params.L2OutputIndex
outputRootProofParam := params.OutputRootProof
withdrawalProofParam := params.WithdrawalProof

// Setup Pessimism to listen for fraudulent withdrawals
// We use two invariants here; one configured with a dummy L1 message passer
// and one configured with the real L1->L2 message passer contract. This allows us to
// ensure that an alert is only produced using faulty message passer.
err = ts.App.BootStrap([]models.InvRequestParams{{
// This is the one that should produce an alert
Network: "layer1",
PType: "live",
InvType: "withdrawal_enforcement",
StartHeight: nil,
EndHeight: nil,
AlertingDest: "slack",
SessionParams: map[string]interface{}{
"l1_portal": predeploys.DevOptimismPortal,
"l2_messager": fakeAddr.String(),
},
},
{
// This is the one that shouldn't produce an alert
Network: "layer1",
PType: "live",
InvType: "withdrawal_enforcement",
StartHeight: nil,
EndHeight: nil,
AlertingDest: "slack",
SessionParams: map[string]interface{}{
"l1_portal": predeploys.DevOptimismPortal,
"l2_messager": predeploys.L2ToL1MessagePasserAddr.String(),
},
},
})
assert.NoError(t, err, "Error bootstrapping invariant session")
time.Sleep(1 * time.Second)

// Prove withdrawal. This checks the proof so we only finalize if this succeeds
tx, err = depositContract.ProveWithdrawalTransaction(
transactor.L1Opts,
*withdrawalTransaction,
l2OutputIndexParam,
outputRootProofParam,
withdrawalProofParam,
)
assert.Nil(t, err, "withdrawal should successfully prove")

// Wait for the transaction to appear in L1
_, err = e2e.WaitForTransaction(tx.Hash(), l1Client, txTimeoutDuration)
assert.Nil(t, err, "withdrawal finalized on L1")
time.Sleep(1 * time.Second)

// Ensure Pessimism has detected what it considers a "faulty" withdrawal
alerts := ts.TestSvr.SlackAlerts()
assert.Equal(t, 1, len(alerts), "expected 1 alert")
assert.Contains(t, alerts[0].Text, "withdrawal_enforcement", "expected alert to be for withdrawal_enforcement")
assert.Contains(t, alerts[0].Text, fakeAddr.String(), "expected alert to be for dummy L2ToL1MessagePasser")
}
5 changes: 5 additions & 0 deletions e2e/test_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ func NewTestServer() *TestServer {
return ts
}

// Close ... Closes the server
func (svr *TestServer) Close() {
svr.Server.Close()
}

// mockSlackPost ... Mocks a slack post request
func (svr *TestServer) mockSlackPost(w http.ResponseWriter, r *http.Request) {
var alert *client.SlackPayload
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 // indirect
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+u
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 h1:Ep/joEub9YwcjRY6ND3+Y/w0ncE540RtGatVhtZL0/Q=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
Expand Down
1 change: 1 addition & 0 deletions internal/alert/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func (am *alertManager) EventLoop() error {
}
}

// Shutdown ... Shuts down the alert manager subsystem
func (am *alertManager) Shutdown() error {
am.cancel()
return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/api/service/invariant.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func (svc *PessimismService) ProcessInvariantRequest(ir models.InvRequestBody) (

// runInvariantSession ... Runs an invariant session provided
func (svc *PessimismService) RunInvariantSession(params models.InvRequestParams) (core.SUUID, error) {
inv, err := registry.GetInvariant(params.InvariantType(), params.SessionParams)
inv, err := registry.GetInvariant(svc.ctx, params.InvariantType(), params.SessionParams)
if err != nil {
return core.NilSUUID(), err
}
Expand Down
10 changes: 5 additions & 5 deletions internal/api/service/invariant_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func Test_ProcessInvariantRequest(t *testing.T) {

ts.mockEtlMan.EXPECT().
CreateDataPipeline(gomock.Any()).
Return(core.NilPUUID(), testErr1()).
Return(core.NilPUUID(), false, testErr1()).
Times(1)

return ts
Expand All @@ -102,7 +102,7 @@ func Test_ProcessInvariantRequest(t *testing.T) {

ts.mockEtlMan.EXPECT().
CreateDataPipeline(gomock.Any()).
Return(core.NilPUUID(), nil).
Return(core.NilPUUID(), false, nil).
Times(1)

ts.mockEtlMan.EXPECT().
Expand Down Expand Up @@ -134,7 +134,7 @@ func Test_ProcessInvariantRequest(t *testing.T) {

ts.mockEtlMan.EXPECT().
CreateDataPipeline(gomock.Any()).
Return(core.NilPUUID(), nil).
Return(core.NilPUUID(), false, nil).
Times(1)

ts.mockEtlMan.EXPECT().
Expand Down Expand Up @@ -176,7 +176,7 @@ func Test_ProcessInvariantRequest(t *testing.T) {

ts.mockEtlMan.EXPECT().
CreateDataPipeline(gomock.Any()).
Return(core.NilPUUID(), nil).
Return(core.NilPUUID(), false, nil).
Times(1)

ts.mockEtlMan.EXPECT().
Expand Down Expand Up @@ -218,7 +218,7 @@ func Test_ProcessInvariantRequest(t *testing.T) {

ts.mockEtlMan.EXPECT().
CreateDataPipeline(gomock.Any()).
Return(core.NilPUUID(), nil).
Return(core.NilPUUID(), false, nil).
Times(1)

ts.mockEtlMan.EXPECT().
Expand Down
Loading