Commit 1bfe79f2 authored by clabby's avatar clabby Committed by GitHub

feat(ctb): Two Step Withdrawals V2 (#3836)

* 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`
parent f7410440
---
'@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
......@@ -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,
......@@ -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)
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
......@@ -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{})
......@@ -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
......@@ -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()
}
......@@ -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)
......@@ -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)
......
......@@ -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{},
......
......@@ -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)
......@@ -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,
......@@ -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)
......@@ -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)
}
......
......@@ -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
......@@ -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
......@@ -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,
......
This diff is collapsed.
......@@ -88,21 +88,23 @@
➡ contracts/L1/OptimismPortal.sol:OptimismPortal
=======================
+----------------------+----------------------------------------+------+--------+-------+------------------------------------------------+
| Name | Type | Slot | Offset | Bytes | Contract |
+========================================================================================================================================+
| _initialized | uint8 | 0 | 0 | 1 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+----------------------------------------+------+--------+-------+------------------------------------------------|
| _initializing | bool | 0 | 1 | 1 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+----------------------------------------+------+--------+-------+------------------------------------------------|
| params | struct ResourceMetering.ResourceParams | 1 | 0 | 32 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+----------------------------------------+------+--------+-------+------------------------------------------------|
| __gap | uint256[48] | 2 | 0 | 1536 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+----------------------------------------+------+--------+-------+------------------------------------------------|
| l2Sender | address | 50 | 0 | 20 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+----------------------------------------+------+--------+-------+------------------------------------------------|
| finalizedWithdrawals | mapping(bytes32 => bool) | 51 | 0 | 32 | contracts/L1/OptimismPortal.sol:OptimismPortal |
+----------------------+----------------------------------------+------+--------+-------+------------------------------------------------+
+----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------+
| Name | Type | Slot | Offset | Bytes | Contract |
+============================================================================================================================================================+
| _initialized | uint8 | 0 | 0 | 1 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| _initializing | bool | 0 | 1 | 1 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| params | struct ResourceMetering.ResourceParams | 1 | 0 | 32 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| __gap | uint256[48] | 2 | 0 | 1536 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| l2Sender | address | 50 | 0 | 20 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| finalizedWithdrawals | mapping(bytes32 => bool) | 51 | 0 | 32 | contracts/L1/OptimismPortal.sol:OptimismPortal |
|----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------|
| provenWithdrawals | mapping(bytes32 => struct OptimismPortal.ProvenWithdrawal) | 52 | 0 | 32 | contracts/L1/OptimismPortal.sol:OptimismPortal |
+----------------------+------------------------------------------------------------+------+--------+-------+------------------------------------------------+
=======================
➡ contracts/L1/SystemConfig.sol:SystemConfig
......
......@@ -19,6 +19,15 @@ import { Semver } from "../universal/Semver.sol";
* Users are encouraged to use the L1CrossDomainMessenger for a higher-level interface.
*/
contract OptimismPortal is Initializable, ResourceMetering, Semver {
/**
* @notice Represents a proven withdrawal
*/
struct ProvenWithdrawal {
bytes32 outputRoot;
uint128 timestamp;
uint128 l2BlockNumber;
}
/**
* @notice Version of the deposit event.
*/
......@@ -61,6 +70,11 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
*/
mapping(bytes32 => bool) public finalizedWithdrawals;
/**
* @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data.
*/
mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals;
/**
* @notice Emitted when a transaction is deposited from L1 to L2. The parameters of this event
* are read by the rollup node and used to derive deposit transactions on L2.
......@@ -77,6 +91,17 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
bytes opaqueData
);
/**
* @notice Emitted when a withdrawal transaction is proven.
*
* @param withdrawalHash Hash of the withdrawal transaction.
*/
event WithdrawalProven(
bytes32 indexed withdrawalHash,
address indexed from,
address indexed to
);
/**
* @notice Emitted when a withdrawal transaction is finalized.
*
......@@ -125,47 +150,38 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
}
/**
* @notice Finalizes a withdrawal transaction.
* @notice Proves a withdrawal transaction.
*
* @param _tx Withdrawal transaction to finalize.
* @param _l2BlockNumber L2 block number of the outputRoot.
* @param _outputRootProof Inclusion proof of the L2ToL1MessagePasser contract's storage root.
* @param _withdrawalProof Inclusion proof of the withdrawal in L2ToL1MessagePasser contract.
*/
function finalizeWithdrawalTransaction(
function proveWithdrawalTransaction(
Types.WithdrawalTransaction memory _tx,
uint256 _l2BlockNumber,
Types.OutputRootProof calldata _outputRootProof,
bytes[] calldata _withdrawalProof
) external {
// Prevent nested withdrawals within withdrawals.
require(
l2Sender == DEFAULT_L2_SENDER,
"OptimismPortal: can only trigger one withdrawal per transaction"
);
// Prevent users from creating a deposit transaction where this address is the message
// sender on L2.
// In the context of the proxy delegate calling to this implementation,
// address(this) will return the address of the proxy.
//
// Because this is checked here, we do not need to check again in
// `finalizeWithdrawalTransaction`
require(
_tx.target != address(this),
"OptimismPortal: you cannot send messages to the portal contract"
);
// Get the output root. This will fail if there is no
// output root for the given block number.
Types.OutputProposal memory proposal = L2_ORACLE.getL2Output(_l2BlockNumber);
// Ensure that enough time has passed since the proposal was submitted before allowing a
// withdrawal. Under the assumption that the fault proof mechanism is operating correctly,
// we can infer that any withdrawal that has passed the finalization period must be valid
// and can therefore be operated on.
require(_isOutputFinalized(proposal), "OptimismPortal: proposal is not yet finalized");
// Get the output root and load onto the stack to prevent multiple mloads. This will
// fail if there is no output root for the given block number.
bytes32 outputRoot = L2_ORACLE.getL2Output(_l2BlockNumber).outputRoot;
// Verify that the output root can be generated with the elements in the proof.
require(
proposal.outputRoot == Hashing.hashOutputRootProof(_outputRootProof),
outputRoot == Hashing.hashOutputRootProof(_outputRootProof),
"OptimismPortal: invalid output root proof"
);
......@@ -185,6 +201,73 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
"OptimismPortal: invalid withdrawal inclusion proof"
);
// Designate the withdrawalHash as proven by storing the `outputRoot`, `timestamp`,
// and `l2BlockNumber` in the `provenWithdrawals` mapping. A certain withdrawal
// can be proved multiple times and thus overwrite a previously stored `ProvenWithdrawal`,
// but this is safe due to the replay check in `finalizeWithdrawalTransaction`.
provenWithdrawals[withdrawalHash] = ProvenWithdrawal({
outputRoot: outputRoot,
timestamp: uint128(block.timestamp),
l2BlockNumber: uint128(_l2BlockNumber)
});
// Emit a `WithdrawalProven` event.
emit WithdrawalProven(withdrawalHash, _tx.sender, _tx.target);
}
/**
* @notice Finalizes a withdrawal transaction.
*
* @param _tx Withdrawal transaction to finalize.
*/
function finalizeWithdrawalTransaction(Types.WithdrawalTransaction memory _tx) external {
// Prevent nested withdrawals within withdrawals.
require(
l2Sender == DEFAULT_L2_SENDER,
"OptimismPortal: can only trigger one withdrawal per transaction"
);
// All withdrawals have a unique hash, we'll use this as the identifier for the withdrawal
// and to prevent replay attacks.
bytes32 withdrawalHash = Hashing.hashWithdrawal(_tx);
// Grab the proven withdrawal from the `provenWithdrawals` map.
ProvenWithdrawal memory provenWithdrawal = provenWithdrawals[withdrawalHash];
// Ensure that the withdrawal has been proven
require(provenWithdrawal.timestamp != 0, "OptimismPortal: withdrawal has not been proven");
// Ensure that the proven withdrawal's timestamp is greater than the
// L2 Oracle's starting timestamp.
require(
provenWithdrawal.timestamp >= L2_ORACLE.STARTING_TIMESTAMP(),
"OptimismPortal: withdrawal timestamp less than L2 Oracle starting timestamp"
);
// Ensure that the withdrawal's finalization period has elapsed.
require(
_isFinalizationPeriodElapsed(provenWithdrawal.timestamp),
"OptimismPortal: proven withdrawal finalization period has not elapsed"
);
// Grab the OutputProposal from the L2 Oracle
Types.OutputProposal memory proposal = L2_ORACLE.getL2Output(
provenWithdrawal.l2BlockNumber
);
// Check that the output proposal hasn't been updated.
require(
proposal.outputRoot == provenWithdrawal.outputRoot,
"OptimismPortal: output root proven is not the same as current output root"
);
// Perform second checks on the withdrawal's finalization period, this time with
// the `OutputProposal`'s timestamp fetched from the L2 Oracle.
require(
_isFinalizationPeriodElapsed(proposal.timestamp),
"OptimismPortal: output proposal finalization period has not elapsed"
);
// Check that this withdrawal has not already been finalized, this is replay protection.
require(
finalizedWithdrawals[withdrawalHash] == false,
......@@ -225,8 +308,7 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
* @param _l2BlockNumber The number of the L2 block.
*/
function isBlockFinalized(uint256 _l2BlockNumber) external view returns (bool) {
Types.OutputProposal memory proposal = L2_ORACLE.getL2Output(_l2BlockNumber);
return _isOutputFinalized(proposal);
return _isFinalizationPeriodElapsed(L2_ORACLE.getL2Output(_l2BlockNumber).timestamp);
}
/**
......@@ -277,16 +359,13 @@ contract OptimismPortal is Initializable, ResourceMetering, Semver {
}
/**
* @notice Determine if an L2 Output is finalized.
* @notice Determine if the finalization period has elapsed with respect to the
* passed timestamp.
*
* @param _proposal The output proposal to check.
* @param _timestamp The timestamp to check.
*/
function _isOutputFinalized(Types.OutputProposal memory _proposal)
internal
view
returns (bool)
{
return block.timestamp > _proposal.timestamp + FINALIZATION_PERIOD_SECONDS;
function _isFinalizationPeriodElapsed(uint256 _timestamp) internal view returns (bool) {
return block.timestamp > _timestamp + FINALIZATION_PERIOD_SECONDS;
}
/**
......
......@@ -453,7 +453,7 @@ contract ERC721Bridge_Initializer is Messenger_Initializer {
}
contract FFIInterface is Test {
function getFinalizeWithdrawalTransactionInputs(Types.WithdrawalTransaction memory _tx)
function getProveWithdrawalTransactionInputs(Types.WithdrawalTransaction memory _tx)
external
returns (
bytes32,
......@@ -466,7 +466,7 @@ contract FFIInterface is Test {
string[] memory cmds = new string[](9);
cmds[0] = "node";
cmds[1] = "dist/scripts/differential-testing.js";
cmds[2] = "getFinalizeWithdrawalTransactionInputs";
cmds[2] = "getProveWithdrawalTransactionInputs";
cmds[3] = vm.toString(_tx.nonce);
cmds[4] = vm.toString(_tx.sender);
cmds[5] = vm.toString(_tx.target);
......
......@@ -165,7 +165,7 @@ const command = args[0]
process.stdout.write(output)
break
}
case 'getFinalizeWithdrawalTransactionInputs': {
case 'getProveWithdrawalTransactionInputs': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
......
This diff is collapsed.
......@@ -143,7 +143,12 @@ export enum MessageStatus {
STATE_ROOT_NOT_PUBLISHED,
/**
* Message is an L2 to L1 message and awaiting the challenge period.
* Message is ready to be proved on L1 to initiate the challenge period.
*/
READY_TO_PROVE,
/**
* Message is a proved L2 to L1 message and is undergoing the challenge period.
*/
IN_CHALLENGE_PERIOD,
......@@ -215,6 +220,14 @@ export interface TokenBridgeMessage {
transactionHash: string
}
/**
* Represents a withdrawal entry within the logs of a L2 to L1
* CrossChainMessage
*/
export interface WithdrawalEntry {
MessagePassed: any
}
/**
* Enum describing the status of a CrossDomainMessage message receipt.
*/
......@@ -231,6 +244,15 @@ export interface MessageReceipt {
transactionReceipt: TransactionReceipt
}
/**
* ProvenWithdrawal in OptimismPortal
*/
export interface ProvenWithdrawal {
outputRoot: string
timestamp: BigNumber
l2BlockNumber: BigNumber
}
/**
* Header for a state root batch.
*/
......
......@@ -304,17 +304,29 @@ task('deposit-erc20', 'Deposits WETH9 onto L2.')
const now = Math.floor(Date.now() / 1000)
console.log('Waiting for message to be able to be proved')
await messenger.waitForMessageStatus(withdraw, MessageStatus.READY_TO_PROVE)
console.log('Proving withdrawal...')
const prove = await messenger.proveMessage(withdraw)
const proveReceipt = await prove.wait()
console.log(proveReceipt)
if (proveReceipt.status !== 1) {
throw new Error('Prove withdrawal transaction reverted')
}
console.log('Waiting for message to be able to be relayed')
await messenger.waitForMessageStatus(
withdraw,
MessageStatus.READY_FOR_RELAY
)
console.log('Finalizing withdrawal...')
const finalize = await messenger.finalizeMessage(withdraw)
const receipt = await finalize.wait()
const finalizeReceipt = await finalize.wait()
console.log(`Took ${Math.floor(Date.now() / 1000) - now} seconds`)
for (const log of receipt.logs) {
for (const log of finalizeReceipt.logs) {
switch (log.address) {
case OptimismPortal.address: {
const parsed = OptimismPortal.interface.parseLog(log)
......
......@@ -229,8 +229,30 @@ task('deposit-eth', 'Deposits WETH9 onto L2.')
`Withdrawal on L2 complete: ${ethWithdrawReceipt.transactionHash}`
)
console.log('Waiting to be able to withdraw')
const interval = setInterval(async () => {
console.log('Waiting to be able to prove withdrawal')
const proveInterval = setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(ethWithdrawReceipt)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
}, 3000)
try {
await messenger.waitForMessageStatus(
ethWithdrawReceipt,
MessageStatus.READY_TO_PROVE
)
} finally {
clearInterval(proveInterval)
}
console.log('Proving eth withdrawal...')
const ethProve = await messenger.proveMessage(ethWithdrawReceipt)
const ethProveReceipt = await ethProve.wait()
if (ethProveReceipt.status !== 1) {
throw new Error('Prove withdrawal transaction reverted')
}
console.log('Waiting to be able to finalize withdrawal')
const finalizeInterval = setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(ethWithdrawReceipt)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
}, 3000)
......@@ -241,9 +263,10 @@ task('deposit-eth', 'Deposits WETH9 onto L2.')
MessageStatus.READY_FOR_RELAY
)
} finally {
clearInterval(interval)
clearInterval(finalizeInterval)
}
console.log('Finalizing eth withdrawal...')
const ethFinalize = await messenger.finalizeMessage(ethWithdrawReceipt)
const ethFinalizeReceipt = await ethFinalize.wait()
if (ethFinalizeReceipt.status !== 1) {
......
......@@ -87,7 +87,25 @@ task('finalize-withdrawal', 'Finalize a withdrawal')
const status = await messenger.getMessageStatus(txHash)
console.log(`Status: ${MessageStatus[status]}`)
if (status === MessageStatus.READY_FOR_RELAY) {
if (status === MessageStatus.READY_TO_PROVE) {
const proveTx = await messenger.proveMessage(txHash)
const proveReceipt = await proveTx.wait()
console.log('Prove receipt', proveReceipt)
const finalizeInterval = setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(txHash)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
}, 3000)
try {
await messenger.waitForMessageStatus(
txHash,
MessageStatus.READY_FOR_RELAY
)
} finally {
clearInterval(finalizeInterval)
}
const tx = await messenger.finalizeMessage(txHash)
const receipt = await tx.wait()
console.log(receipt)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment