Commit af12f2d6 authored by protolambda's avatar protolambda

op-e2e,op-node: withdrawal action testing

parent 4e65ceb9
...@@ -11,6 +11,15 @@ import ( ...@@ -11,6 +11,15 @@ import (
"testing" "testing"
"time" "time"
"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/ethclient"
"github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/indexer" "github.com/ethereum-optimism/optimism/indexer"
"github.com/ethereum-optimism/optimism/indexer/db" "github.com/ethereum-optimism/optimism/indexer/db"
"github.com/ethereum-optimism/optimism/indexer/services/l1" "github.com/ethereum-optimism/optimism/indexer/services/l1"
...@@ -20,12 +29,6 @@ import ( ...@@ -20,12 +29,6 @@ import (
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/withdrawals" "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/params"
"github.com/ethereum/go-ethereum/rpc"
"github.com/stretchr/testify/require"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
...@@ -196,8 +199,9 @@ func TestBedrockIndexer(t *testing.T) { ...@@ -196,8 +199,9 @@ func TestBedrockIndexer(t *testing.T) {
rpcClient, err := rpc.Dial(sys.Nodes["sequencer"].HTTPEndpoint()) rpcClient, err := rpc.Dial(sys.Nodes["sequencer"].HTTPEndpoint())
require.NoError(t, err) require.NoError(t, err)
proofClient := withdrawals.NewClient(rpcClient) proofCl := gethclient.New(rpcClient)
wParams, err := withdrawals.FinalizeWithdrawalParameters(context.Background(), proofClient, wdTx.Hash(), finHeader) receiptCl := ethclient.NewClient(rpcClient)
wParams, err := withdrawals.FinalizeWithdrawalParameters(context.Background(), proofCl, receiptCl, wdTx.Hash(), finHeader)
require.NoError(t, err) require.NoError(t, err)
l1Opts.Value = big.NewInt(0) l1Opts.Value = big.NewInt(0)
......
...@@ -3,6 +3,7 @@ package actions ...@@ -3,6 +3,7 @@ package actions
import ( import (
"errors" "errors"
"github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -117,6 +118,11 @@ func (s *L2Engine) EthClient() *ethclient.Client { ...@@ -117,6 +118,11 @@ func (s *L2Engine) EthClient() *ethclient.Client {
return ethclient.NewClient(cl) return ethclient.NewClient(cl)
} }
func (s *L2Engine) GethClient() *gethclient.Client {
cl, _ := s.node.Attach() // never errors
return gethclient.New(cl)
}
func (e *L2Engine) RPCClient() client.RPC { func (e *L2Engine) RPCClient() client.RPC {
cl, _ := e.node.Attach() // never errors cl, _ := e.node.Attach() // never errors
return testutils.RPCErrFaker{ return testutils.RPCErrFaker{
......
...@@ -46,16 +46,16 @@ func NewL1Bindings(t Testing, l1Cl *ethclient.Client, deployments *e2eutils.Depl ...@@ -46,16 +46,16 @@ func NewL1Bindings(t Testing, l1Cl *ethclient.Client, deployments *e2eutils.Depl
type L2Bindings struct { type L2Bindings struct {
L2ToL1MessagePasser *bindings.L2ToL1MessagePasser L2ToL1MessagePasser *bindings.L2ToL1MessagePasser
WithdrawalsClient *withdrawals.Client ProofClient withdrawals.ProofClient
} }
func NewL2Bindings(t Testing, l2Cl *ethclient.Client, withdrawalsCl *withdrawals.Client) *L2Bindings { func NewL2Bindings(t Testing, l2Cl *ethclient.Client, proofCl withdrawals.ProofClient) *L2Bindings {
l2ToL1MessagePasser, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, l2Cl) l2ToL1MessagePasser, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, l2Cl)
require.NoError(t, err) require.NoError(t, err)
return &L2Bindings{ return &L2Bindings{
L2ToL1MessagePasser: l2ToL1MessagePasser, L2ToL1MessagePasser: l2ToL1MessagePasser,
WithdrawalsClient: withdrawalsCl, ProofClient: proofCl,
} }
} }
...@@ -278,6 +278,8 @@ type CrossLayerUser struct { ...@@ -278,6 +278,8 @@ type CrossLayerUser struct {
// track the last deposit, to easily chain together deposit actions // track the last deposit, to easily chain together deposit actions
lastL1DepositTxHash common.Hash lastL1DepositTxHash common.Hash
lastL2WithdrawalTxHash common.Hash
} }
func NewCrossLayerUser(log log.Logger, priv *ecdsa.PrivateKey, rng *rand.Rand) *CrossLayerUser { func NewCrossLayerUser(log log.Logger, priv *ecdsa.PrivateKey, rng *rand.Rand) *CrossLayerUser {
...@@ -354,6 +356,94 @@ func (s *CrossLayerUser) CheckDepositTx(t Testing, l1TxHash common.Hash, index i ...@@ -354,6 +356,94 @@ func (s *CrossLayerUser) CheckDepositTx(t Testing, l1TxHash common.Hash, index i
} }
} }
func (s *CrossLayerUser) ActStartWithdrawal(t Testing) {
targetAddr := common.Address{}
if s.L1.txToAddr != nil {
targetAddr = *s.L2.txToAddr
}
tx, err := s.L2.env.Bindings.L2ToL1MessagePasser.InitiateWithdrawal(&s.L2.txOpts, targetAddr, new(big.Int).SetUint64(s.L1.txOpts.GasLimit), s.L1.txCallData)
require.NoError(t, err, "create initiate withdraw tx")
err = s.L2.env.EthCl.SendTransaction(t.Ctx(), tx)
require.NoError(t, err, "must send tx")
s.lastL2WithdrawalTxHash = tx.Hash()
}
// ActCheckStartWithdrawal checks that a previous witdrawal tx was either successful or failed.
func (s *CrossLayerUser) ActCheckStartWithdrawal(success bool) Action {
return func(t Testing) {
s.L2.CheckReceipt(t, success, s.lastL2WithdrawalTxHash)
}
}
func (s *CrossLayerUser) Address() common.Address { func (s *CrossLayerUser) Address() common.Address {
return s.L1.address return s.L1.address
} }
// ActCompleteWithdrawal creates a L1 withdrawal completion 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.
// 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{})
require.NoError(t, err)
// 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 completed yet", l2OutputBlock.NumberU64(), l2WithdrawalBlock.NumberU64())
return common.Hash{}
}
l1Head, err := s.L1.env.EthCl.HeaderByNumber(t.Ctx(), nil)
require.NoError(t, err)
// Check if the withdrawal may be completed yet
if l2OutputBlock.Time()+finalizationPeriod.Uint64() >= l1Head.Time {
t.InvalidAction("withdrawal tx %s was included in L2 block %d (time %d) but L1 only knows of L2 proposal %d (time %d) at head %d (time %d) which has not reached output confirmation yet (period is %d)",
l2TxHash, l2WithdrawalBlock.NumberU64(), l2WithdrawalBlock.Time(), l2OutputBlock.NumberU64(), l2OutputBlock.Time(), finalizationPeriod.Uint64(), l1Head.Number.Uint64(), l1Head.Time)
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.FinalizeWithdrawalParameters(t.Ctx(), s.L2.env.Bindings.ProofClient, s.L2.env.EthCl, s.lastL2WithdrawalTxHash, header)
require.NoError(t, err)
// Create the withdrawal tx
tx, err := s.L1.env.Bindings.OptimismPortal.FinalizeWithdrawalTransaction(
&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 tx")
return tx.Hash()
}
...@@ -6,10 +6,10 @@ import ( ...@@ -6,10 +6,10 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils" "github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
"github.com/ethereum-optimism/optimism/op-node/testlog" "github.com/ethereum-optimism/optimism/op-node/testlog"
"github.com/ethereum-optimism/optimism/op-node/withdrawals"
) )
func TestCrossLayerUser(gt *testing.T) { func TestCrossLayerUser(gt *testing.T) {
...@@ -19,13 +19,23 @@ func TestCrossLayerUser(gt *testing.T) { ...@@ -19,13 +19,23 @@ func TestCrossLayerUser(gt *testing.T) {
log := testlog.Logger(t, log.LvlDebug) log := testlog.Logger(t, log.LvlDebug)
miner, seqEngine, seq := setupSequencerTest(t, sd, log) miner, seqEngine, seq := setupSequencerTest(t, sd, log)
batcher := NewL2Batcher(log, sd.RollupCfg, &BatcherCfg{
MinL1TxSize: 0,
MaxL1TxSize: 128_000,
BatcherKey: dp.Secrets.Batcher,
}, seq.RollupClient(), miner.EthClient(), seqEngine.EthClient())
proposer := NewL2Proposer(t, log, &ProposerCfg{
OutputOracleAddr: sd.DeploymentsL1.L2OutputOracleProxy,
ProposerKey: dp.Secrets.Proposer,
AllowNonFinalized: true,
}, miner.EthClient(), seq.RollupClient())
// need to start derivation before we can make L2 blocks // need to start derivation before we can make L2 blocks
seq.ActL2PipelineFull(t) seq.ActL2PipelineFull(t)
l1Cl := miner.EthClient() l1Cl := miner.EthClient()
l2Cl := seqEngine.EthClient() l2Cl := seqEngine.EthClient()
withdrawalsCl := &withdrawals.Client{} // TODO: need a rollup node actor to wrap for output root proof RPC l2ProofCl := seqEngine.GethClient()
addresses := e2eutils.CollectAddresses(sd, dp) addresses := e2eutils.CollectAddresses(sd, dp)
...@@ -39,7 +49,7 @@ func TestCrossLayerUser(gt *testing.T) { ...@@ -39,7 +49,7 @@ func TestCrossLayerUser(gt *testing.T) {
EthCl: l2Cl, EthCl: l2Cl,
Signer: types.LatestSigner(sd.L2Cfg.Config), Signer: types.LatestSigner(sd.L2Cfg.Config),
AddressCorpora: addresses, AddressCorpora: addresses,
Bindings: NewL2Bindings(t, l2Cl, withdrawalsCl), Bindings: NewL2Bindings(t, l2Cl, l2ProofCl),
} }
alice := NewCrossLayerUser(log, dp.Secrets.Alice, rand.New(rand.NewSource(1234))) alice := NewCrossLayerUser(log, dp.Secrets.Alice, rand.New(rand.NewSource(1234)))
...@@ -79,4 +89,50 @@ func TestCrossLayerUser(gt *testing.T) { ...@@ -79,4 +89,50 @@ func TestCrossLayerUser(gt *testing.T) {
} }
// Now that the L2 chain adopted the latest L1 block, check that we processed the deposit // Now that the L2 chain adopted the latest L1 block, check that we processed the deposit
alice.ActCheckDepositStatus(true, true)(t) alice.ActCheckDepositStatus(true, true)(t)
// regular withdrawal, in new L2 block
alice.ActStartWithdrawal(t)
seq.ActL2StartBlock(t)
seqEngine.ActL2IncludeTx(alice.Address())(t)
seq.ActL2EndBlock(t)
alice.ActCheckStartWithdrawal(true)(t)
// build a L1 block and more L2 blocks,
// to ensure the L2 withdrawal is old enough to be able to get into an output root proposal on L1
miner.ActEmptyBlock(t)
seq.ActL1HeadSignal(t)
seq.ActBuildToL1Head(t)
// submit everything to L1
batcher.ActSubmitAll(t)
// include batch on L1
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Batcher)(t)
miner.ActL1EndBlock(t)
// derive from L1, blocks will now become safe to propose
seq.ActL2PipelineFull(t)
// make proposals until there is nothing left to propose
for proposer.CanPropose(t) {
// propose it to L1
proposer.ActMakeProposalTx(t)
// include proposal on L1
miner.ActL1StartBlock(12)(t)
miner.ActL1IncludeTx(dp.Addresses.Proposer)(t)
miner.ActL1EndBlock(t)
// Check proposal was successful
receipt, err := miner.EthClient().TransactionReceipt(t.Ctx(), proposer.LastProposalTx())
require.NoError(t, err)
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "proposal failed")
}
// make the L1 side of the withdrawal tx
alice.ActCompleteWithdrawal(t)
// include completed 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)
} }
...@@ -15,6 +15,7 @@ import ( ...@@ -15,6 +15,7 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-libp2p-core/peer"
...@@ -791,12 +792,13 @@ func TestWithdrawals(t *testing.T) { ...@@ -791,12 +792,13 @@ func TestWithdrawals(t *testing.T) {
header, err = l2Verif.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber)) header, err = l2Verif.HeaderByNumber(ctx, new(big.Int).SetUint64(blockNumber))
require.Nil(t, err) require.Nil(t, err)
rpc, err := rpc.Dial(sys.Nodes["verifier"].WSEndpoint()) rpcClient, err := rpc.Dial(sys.Nodes["verifier"].WSEndpoint())
require.Nil(t, err) require.Nil(t, err)
l2client := withdrawals.NewClient(rpc) proofCl := gethclient.New(rpcClient)
receiptCl := ethclient.NewClient(rpcClient)
// Now create withdrawal // Now create withdrawal
params, err := withdrawals.FinalizeWithdrawalParameters(context.Background(), l2client, tx.Hash(), header) params, err := withdrawals.FinalizeWithdrawalParameters(context.Background(), proofCl, receiptCl, tx.Hash(), header)
require.Nil(t, err) require.Nil(t, err)
portal, err := bindings.NewOptimismPortal(predeploys.DevOptimismPortalAddr, l1Client) portal, err := bindings.NewOptimismPortal(predeploys.DevOptimismPortalAddr, l1Client)
......
...@@ -8,8 +8,6 @@ import ( ...@@ -8,8 +8,6 @@ import (
"math/big" "math/big"
"time" "time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
...@@ -17,7 +15,9 @@ import ( ...@@ -17,7 +15,9 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/ethclient/gethclient" "github.com/ethereum/go-ethereum/ethclient/gethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
) )
var MessagePassedTopic = crypto.Keccak256Hash([]byte("MessagePassed(uint256,address,address,uint256,uint256,bytes,bytes32)")) var MessagePassedTopic = crypto.Keccak256Hash([]byte("MessagePassed(uint256,address,address,uint256,uint256,bytes,bytes32)"))
...@@ -122,29 +122,11 @@ loop: ...@@ -122,29 +122,11 @@ loop:
} }
type ProofClient interface { type ProofClient interface {
TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
GetProof(context.Context, common.Address, []string, *big.Int) (*gethclient.AccountResult, error) GetProof(context.Context, common.Address, []string, *big.Int) (*gethclient.AccountResult, error)
} }
type ec = *ethclient.Client type ReceiptClient interface {
type gc = *gethclient.Client TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
type Client struct {
ec
gc
}
// Ensure that ProofClient and Client interfaces are valid
var _ ProofClient = &Client{}
// NewClient wraps a RPC client with both ethclient and gethclient methods.
// Implements ProofClient
func NewClient(client *rpc.Client) *Client {
return &Client{
ethclient.NewClient(client),
gethclient.New(client),
}
} }
// FinalizedWithdrawalParameters is the set of parameters to pass to the FinalizedWithdrawal function // FinalizedWithdrawalParameters is the set of parameters to pass to the FinalizedWithdrawal function
...@@ -163,9 +145,9 @@ type FinalizedWithdrawalParameters struct { ...@@ -163,9 +145,9 @@ type FinalizedWithdrawalParameters struct {
// FinalizeWithdrawalParameters queries L2 to generate all withdrawal parameters and proof necessary to finalize an withdrawal on L1. // FinalizeWithdrawalParameters queries L2 to generate all withdrawal parameters and proof necessary to finalize an 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 // 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. // 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, l2client ProofClient, txHash common.Hash, header *types.Header) (FinalizedWithdrawalParameters, error) { func FinalizeWithdrawalParameters(ctx context.Context, proofCl ProofClient, l2ReceiptCl ReceiptClient, txHash common.Hash, header *types.Header) (FinalizedWithdrawalParameters, error) {
// Transaction receipt // Transaction receipt
receipt, err := l2client.TransactionReceipt(ctx, txHash) receipt, err := l2ReceiptCl.TransactionReceipt(ctx, txHash)
if err != nil { if err != nil {
return FinalizedWithdrawalParameters{}, err return FinalizedWithdrawalParameters{}, err
} }
...@@ -183,7 +165,7 @@ func FinalizeWithdrawalParameters(ctx context.Context, l2client ProofClient, txH ...@@ -183,7 +165,7 @@ func FinalizeWithdrawalParameters(ctx context.Context, l2client ProofClient, txH
return FinalizedWithdrawalParameters{}, err return FinalizedWithdrawalParameters{}, err
} }
slot := StorageSlotOfWithdrawalHash(withdrawalHash) slot := StorageSlotOfWithdrawalHash(withdrawalHash)
p, err := l2client.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, header.Number) p, err := proofCl.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, header.Number)
if err != nil { if err != nil {
return FinalizedWithdrawalParameters{}, err return FinalizedWithdrawalParameters{}, err
} }
......
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