Skip to content

Commit

Permalink
feat(ctb): Two Step Withdrawals V2 (#3836)
Browse files Browse the repository at this point in the history
* Start contract changes for two step withdrawals v2

* Fix maurelian's nits

* Refactor Kelvin's SDK changes; SDK/integration test time

* Merge w/ `develop`

* Add tests for changed output proposal *after* proving the withdrawal hash

Whoops

* Gas snapshot / comments

* Regenerate bindings; Fix E2E Withdrawal test; Add extra indexed params to `WithdrawalProven`

* Start fixing indexer integration tests

* Fix conflicts; Start updating mark's new `op-e2e` withdrawal action tests

* Remove proposal timestamp >= withdrawal timestamp check

* Fix mark's `op-e2e` test + add docs to `proveMessage` in SDK

* Update changeset

* Lint contracts

* Merge with `develop`

* Re-order mapping declarations so that `finalizedWithdrawals` retains its old storage slot

* Merge with `develop`

* Start updating devnet tests

* Fix devnet tests

* Update ERC20 binding

* Clean up SDK

* Merge with `develop`

* Remove `integration-tests-bedrock` package

* Add check for equality between locally computed withdrawal hash vs. on-chain withdrawal hash

* Add Kelvin's check + complimentary test

Update bindings

* Fix finalization period in `TestCrossLayerUser`
  • Loading branch information
clabby committed Nov 11, 2022
1 parent f741044 commit 1bfe79f
Show file tree
Hide file tree
Showing 20 changed files with 1,155 additions and 295 deletions.
8 changes: 8 additions & 0 deletions .changeset/poor-dots-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@eth-optimism/indexer': minor
'@eth-optimism/contracts-bedrock': minor
'@eth-optimism/integration-tests-bedrock': minor
'@eth-optimism/sdk': minor
---

Adds an implementation of the Two Step Withdrawals V2 proposal
31 changes: 29 additions & 2 deletions indexer/integration_tests/bedrock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,12 @@ func TestBedrockIndexer(t *testing.T) {
require.NoError(t, err)
proofCl := gethclient.New(rpcClient)
receiptCl := ethclient.NewClient(rpcClient)
wParams, err := withdrawals.FinalizeWithdrawalParameters(context.Background(), proofCl, receiptCl, wdTx.Hash(), finHeader)
wParams, err := withdrawals.ProveWithdrawalParameters(context.Background(), proofCl, receiptCl, wdTx.Hash(), finHeader)
require.NoError(t, err)

l1Opts.Value = big.NewInt(0)
finTx, err := portal.FinalizeWithdrawalTransaction(
// Prove our withdrawal
proveTx, err := portal.ProveWithdrawalTransaction(
l1Opts,
bindings.TypesWithdrawalTransaction{
Nonce: wParams.Nonce,
Expand All @@ -221,6 +222,32 @@ func TestBedrockIndexer(t *testing.T) {
)
require.NoError(t, err)

_, err = e2eutils.WaitReceiptOK(e2eutils.TimeoutCtx(t, time.Minute), l1Client, proveTx.Hash())
require.NoError(t, err)

// Wait for the finalization period to elapse
_, err = withdrawals.WaitForFinalizationPeriod(
e2eutils.TimeoutCtx(t, time.Minute),
l1Client,
predeploys.DevOptimismPortalAddr,
wParams.BlockNumber,
)
require.NoError(t, err)

// Send our finalize withdrawal transaction
finTx, err := portal.FinalizeWithdrawalTransaction(
l1Opts,
bindings.TypesWithdrawalTransaction{
Nonce: wParams.Nonce,
Sender: wParams.Sender,
Target: wParams.Target,
Value: wParams.Value,
GasLimit: wParams.GasLimit,
Data: wParams.Data,
},
)
require.NoError(t, err)

finReceipt, err := e2eutils.WaitReceiptOK(e2eutils.TimeoutCtx(t, time.Minute), l1Client, finTx.Hash())
require.NoError(t, err)

Expand Down
261 changes: 247 additions & 14 deletions op-bindings/bindings/optimismportal.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions op-bindings/bindings/optimismportal_more.go

Large diffs are not rendered by default.

67 changes: 60 additions & 7 deletions op-e2e/actions/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,68 @@ func (s *CrossLayerUser) Address() common.Address {
return s.L1.address
}

// ActCompleteWithdrawal creates a L1 withdrawal completion tx for latest withdrawal.
// ActCompleteWithdrawal creates a L1 proveWithdrawal tx for latest withdrawal.
// The tx hash is remembered as the last L1 tx, to check as L1 actor.
func (s *CrossLayerUser) ActProveWithdrawal(t Testing) {
s.L1.lastTxHash = s.ProveWithdrawal(t, s.lastL2WithdrawalTxHash)
}

// ProveWithdrawal creates a L1 proveWithdrawal tx for the given L2 withdrawal tx, returning the tx hash.
func (s *CrossLayerUser) ProveWithdrawal(t Testing, l2TxHash common.Hash) common.Hash {
// Figure out when our withdrawal was included
receipt := s.L2.CheckReceipt(t, true, l2TxHash)
l2WithdrawalBlock, err := s.L2.env.EthCl.BlockByNumber(t.Ctx(), receipt.BlockNumber)
require.NoError(t, err)

// Figure out what the Output oracle on L1 has seen so far
l2OutputBlockNr, err := s.L1.env.Bindings.L2OutputOracle.LatestBlockNumber(&bind.CallOpts{})
require.NoError(t, err)
l2OutputBlock, err := s.L2.env.EthCl.BlockByNumber(t.Ctx(), l2OutputBlockNr)
require.NoError(t, err)

// Check if the L2 output is even old enough to include the withdrawal
if l2OutputBlock.NumberU64() < l2WithdrawalBlock.NumberU64() {
t.InvalidAction("the latest L2 output is %d and is not past L2 block %d that includes the withdrawal yet, no withdrawal can be proved yet", l2OutputBlock.NumberU64(), l2WithdrawalBlock.NumberU64())
return common.Hash{}
}

// We generate a proof for the latest L2 output, which shouldn't require archive-node data if it's recent enough.
header, err := s.L2.env.EthCl.HeaderByNumber(t.Ctx(), l2OutputBlockNr)
require.NoError(t, err)
params, err := withdrawals.ProveWithdrawalParameters(t.Ctx(), s.L2.env.Bindings.ProofClient, s.L2.env.EthCl, s.lastL2WithdrawalTxHash, header)
require.NoError(t, err)

// Create the prove tx
tx, err := s.L1.env.Bindings.OptimismPortal.ProveWithdrawalTransaction(
&s.L1.txOpts,
bindings.TypesWithdrawalTransaction{
Nonce: params.Nonce,
Sender: params.Sender,
Target: params.Target,
Value: params.Value,
GasLimit: params.GasLimit,
Data: params.Data,
},
params.BlockNumber,
params.OutputRootProof,
params.WithdrawalProof,
)
require.NoError(t, err)

// Send the actual tx (since tx opts don't send by default)
err = s.L1.env.EthCl.SendTransaction(t.Ctx(), tx)
require.NoError(t, err, "must send prove tx")
return tx.Hash()
}

// ActCompleteWithdrawal creates a L1 withdrawal finalization tx for latest withdrawal.
// The tx hash is remembered as the last L1 tx, to check as L1 actor.
// The withdrawal functions like CompleteWithdrawal
func (s *CrossLayerUser) ActCompleteWithdrawal(t Testing) {
s.L1.lastTxHash = s.CompleteWithdrawal(t, s.lastL2WithdrawalTxHash)
}

// CompleteWithdrawal creates a L1 withdrawal completion tx for the given L2 withdrawal tx, returning the tx hash.
// CompleteWithdrawal creates a L1 withdrawal finalization tx for the given L2 withdrawal tx, returning the tx hash.
// It's an invalid action to attempt to complete a withdrawal that has not passed the L1 finalization period yet
func (s *CrossLayerUser) CompleteWithdrawal(t Testing, l2TxHash common.Hash) common.Hash {
finalizationPeriod, err := s.L1.env.Bindings.OptimismPortal.FINALIZATIONPERIODSECONDS(&bind.CallOpts{})
Expand Down Expand Up @@ -420,9 +474,11 @@ func (s *CrossLayerUser) CompleteWithdrawal(t Testing, l2TxHash common.Hash) com
}

// We generate a proof for the latest L2 output, which shouldn't require archive-node data if it's recent enough.
// Note that for the `FinalizeWithdrawalTransaction` function, this proof isn't needed. We simply use some of the
// params for the `WithdrawalTransaction` type generated in the bindings.
header, err := s.L2.env.EthCl.HeaderByNumber(t.Ctx(), l2OutputBlockNr)
require.NoError(t, err)
params, err := withdrawals.FinalizeWithdrawalParameters(t.Ctx(), s.L2.env.Bindings.ProofClient, s.L2.env.EthCl, s.lastL2WithdrawalTxHash, header)
params, err := withdrawals.ProveWithdrawalParameters(t.Ctx(), s.L2.env.Bindings.ProofClient, s.L2.env.EthCl, s.lastL2WithdrawalTxHash, header)
require.NoError(t, err)

// Create the withdrawal tx
Expand All @@ -436,14 +492,11 @@ func (s *CrossLayerUser) CompleteWithdrawal(t Testing, l2TxHash common.Hash) com
GasLimit: params.GasLimit,
Data: params.Data,
},
params.BlockNumber,
params.OutputRootProof,
params.WithdrawalProof,
)
require.NoError(t, err)

// Send the actual tx (since tx opts don't send by default)
err = s.L1.env.EthCl.SendTransaction(t.Ctx(), tx)
require.NoError(t, err, "must send tx")
require.NoError(t, err, "must send finalize tx")
return tx.Hash()
}
19 changes: 18 additions & 1 deletion op-e2e/actions/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
// - transact on L2
// - deposit on L1
// - withdraw from L2
// - prove tx on L1
// - wait 1 week + 1 second
// - finalize withdrawal on L1
func TestCrossLayerUser(gt *testing.T) {
t := NewDefaultTesting(gt)
Expand Down Expand Up @@ -133,7 +135,22 @@ func TestCrossLayerUser(gt *testing.T) {
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed")
}

// make the L1 side of the withdrawal tx
// prove our withdrawal on L1
alice.ActProveWithdrawal(t)
// include proved withdrawal in new L1 block
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(alice.Address())(t)
miner.ActL1EndBlock(t)
// check withdrawal succeeded
alice.L1.ActCheckReceiptStatusOfLastTx(true)(t)

// A bit hacky- Mines an empty block with the time delta
// of the finalization period (12s) + 1 in order for the
// withdrawal to be finalized successfully.
miner.ActL1StartBlock(13)(t)
miner.ActL1EndBlock(t)

// make the L1 finalize withdrawal tx
alice.ActCompleteWithdrawal(t)
// include completed withdrawal in new L1 block
miner.ActL1StartBlock(12)(t)
Expand Down
1 change: 1 addition & 0 deletions op-e2e/e2eutils/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func MakeDeployParams(t require.TestingT, tp *TestParams) *DeployParams {
L1GenesisBlockGasUsed: 0,
L1GenesisBlockParentHash: common.Hash{},
L1GenesisBlockBaseFeePerGas: uint64ToBig(1000_000_000), // 1 gwei
FinalizationPeriodSeconds: 12,

L2GenesisBlockNonce: 0,
L2GenesisBlockExtraData: []byte{},
Expand Down
42 changes: 35 additions & 7 deletions op-e2e/system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@ func TestWithdrawals(t *testing.T) {
startBalance, err = l1Client.BalanceAt(ctx, fromAddr, nil)
require.Nil(t, err)

// Wait for finalization and then create the Finalized Withdrawal Transaction
// Get l2BlockNumber for proof generation
ctx, cancel = context.WithTimeout(context.Background(), 20*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
defer cancel()
blockNumber, err := withdrawals.WaitForFinalizationPeriod(ctx, l1Client, predeploys.DevOptimismPortalAddr, receipt.BlockNumber)
Expand All @@ -836,14 +836,16 @@ func TestWithdrawals(t *testing.T) {
receiptCl := ethclient.NewClient(rpcClient)

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

portal, err := bindings.NewOptimismPortal(predeploys.DevOptimismPortalAddr, l1Client)
require.Nil(t, err)

opts.Value = nil
tx, err = portal.FinalizeWithdrawalTransaction(

// Prove withdrawal
tx, err = portal.ProveWithdrawalTransaction(
opts,
bindings.TypesWithdrawalTransaction{
Nonce: params.Nonce,
Expand All @@ -857,17 +859,42 @@ func TestWithdrawals(t *testing.T) {
params.OutputRootProof,
params.WithdrawalProof,
)
require.Nil(t, err)

// Ensure that our withdrawal was proved successfully
proveReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "prove withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status)

// Wait for finalization and then create the Finalized Withdrawal Transaction
ctx, cancel = context.WithTimeout(context.Background(), 20*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
defer cancel()
_, err = withdrawals.WaitForFinalizationPeriod(ctx, l1Client, predeploys.DevOptimismPortalAddr, params.BlockNumber)
require.Nil(t, err)

// Finalize withdrawal
tx, err = portal.FinalizeWithdrawalTransaction(
opts,
bindings.TypesWithdrawalTransaction{
Nonce: params.Nonce,
Sender: params.Sender,
Target: params.Target,
Value: params.Value,
GasLimit: params.GasLimit,
Data: params.Data,
},
)
require.Nil(t, err)

receipt, err = waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
// Ensure that our withdrawal was finalized successfully
finalizeReceipt, err := waitForTransaction(tx.Hash(), l1Client, 3*time.Duration(cfg.DeployConfig.L1BlockTime)*time.Second)
require.Nil(t, err, "finalize withdrawal")
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status)
require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status)

// Verify balance after withdrawal
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
header, err = l1Client.HeaderByNumber(ctx, receipt.BlockNumber)
header, err = l1Client.HeaderByNumber(ctx, finalizeReceipt.BlockNumber)
require.Nil(t, err)

ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
Expand All @@ -877,8 +904,9 @@ func TestWithdrawals(t *testing.T) {

// Ensure that withdrawal - gas fees are added to the L1 balance
// Fun fact, the fee is greater than the withdrawal amount
// NOTE: The gas fees include *both* the ProveWithdrawalTransaction and FinalizeWithdrawalTransaction transactions.
diff = new(big.Int).Sub(endBalance, startBalance)
fees = calcGasFees(receipt.GasUsed, tx.GasTipCap(), tx.GasFeeCap(), header.BaseFee)
fees = calcGasFees(proveReceipt.GasUsed+finalizeReceipt.GasUsed, tx.GasTipCap(), tx.GasFeeCap(), header.BaseFee)
withdrawAmount = withdrawAmount.Sub(withdrawAmount, fees)
require.Equal(t, withdrawAmount, diff)
}
Expand Down
25 changes: 13 additions & 12 deletions op-node/withdrawals/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ type ReceiptClient interface {
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
}

// FinalizedWithdrawalParameters is the set of parameters to pass to the FinalizedWithdrawal function
type FinalizedWithdrawalParameters struct {
// ProvenWithdrawalParameters is the set of parameters to pass to the ProveWithdrawalTransaction
// and FinalizeWithdrawalTransaction functions
type ProvenWithdrawalParameters struct {
Nonce *big.Int
Sender common.Address
Target common.Address
Expand All @@ -142,40 +143,40 @@ type FinalizedWithdrawalParameters struct {
WithdrawalProof [][]byte // List of trie nodes to prove L2 storage
}

// FinalizeWithdrawalParameters queries L2 to generate all withdrawal parameters and proof necessary to finalize an withdrawal on L1.
// ProveWithdrawalParameters queries L2 to generate all withdrawal parameters and proof necessary to prove a withdrawal on L1.
// The header provided is very important. It should be a block (timestamp) for which there is a submitted output in the L2 Output Oracle
// contract. If not, the withdrawal will fail as it the storage proof cannot be verified if there is no submitted state root.
func FinalizeWithdrawalParameters(ctx context.Context, proofCl ProofClient, l2ReceiptCl ReceiptClient, txHash common.Hash, header *types.Header) (FinalizedWithdrawalParameters, error) {
func ProveWithdrawalParameters(ctx context.Context, proofCl ProofClient, l2ReceiptCl ReceiptClient, txHash common.Hash, header *types.Header) (ProvenWithdrawalParameters, error) {
// Transaction receipt
receipt, err := l2ReceiptCl.TransactionReceipt(ctx, txHash)
if err != nil {
return FinalizedWithdrawalParameters{}, err
return ProvenWithdrawalParameters{}, err
}
// Parse the receipt
ev, err := ParseMessagePassed(receipt)
if err != nil {
return FinalizedWithdrawalParameters{}, err
return ProvenWithdrawalParameters{}, err
}
// Generate then verify the withdrawal proof
withdrawalHash, err := WithdrawalHash(ev)
if !bytes.Equal(withdrawalHash[:], ev.WithdrawalHash[:]) {
return FinalizedWithdrawalParameters{}, errors.New("Computed withdrawal hash incorrectly")
return ProvenWithdrawalParameters{}, errors.New("Computed withdrawal hash incorrectly")
}
if err != nil {
return FinalizedWithdrawalParameters{}, err
return ProvenWithdrawalParameters{}, err
}
slot := StorageSlotOfWithdrawalHash(withdrawalHash)
p, err := proofCl.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, header.Number)
if err != nil {
return FinalizedWithdrawalParameters{}, err
return ProvenWithdrawalParameters{}, err
}
// TODO: Could skip this step, but it's nice to double check it
err = VerifyProof(header.Root, p)
if err != nil {
return FinalizedWithdrawalParameters{}, err
return ProvenWithdrawalParameters{}, err
}
if len(p.StorageProof) != 1 {
return FinalizedWithdrawalParameters{}, errors.New("invalid amount of storage proofs")
return ProvenWithdrawalParameters{}, errors.New("invalid amount of storage proofs")
}

// Encode it as expected by the contract
Expand All @@ -184,7 +185,7 @@ func FinalizeWithdrawalParameters(ctx context.Context, proofCl ProofClient, l2Re
trieNodes[i] = common.FromHex(s)
}

return FinalizedWithdrawalParameters{
return ProvenWithdrawalParameters{
Nonce: ev.Nonce,
Sender: ev.Sender,
Target: ev.Target,
Expand Down

0 comments on commit 1bfe79f

Please sign in to comment.