Commit b2242f40 authored by Mark Tyneway's avatar Mark Tyneway

op-chain-ops: more fixes

parent de9a9a16
...@@ -17,6 +17,7 @@ import ( ...@@ -17,6 +17,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-bindings/predeploys"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration" "github.com/ethereum-optimism/optimism/op-chain-ops/genesis/migration"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
...@@ -31,6 +32,8 @@ import ( ...@@ -31,6 +32,8 @@ import (
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
var abiTrue = common.Hash{31: 0x01}
// callFrame represents the response returned from geth's // callFrame represents the response returned from geth's
// `debug_traceTransaction` callTracer // `debug_traceTransaction` callTracer
type callFrame struct { type callFrame struct {
...@@ -46,6 +49,20 @@ type callFrame struct { ...@@ -46,6 +49,20 @@ type callFrame struct {
Calls []callFrame `json:"calls,omitempty"` Calls []callFrame `json:"calls,omitempty"`
} }
func (c *callFrame) BigValue() *big.Int {
v := strings.TrimPrefix(c.Value, "0x")
b, _ := new(big.Int).SetString(v, 16)
return b
}
type suspiciousWithdrawal struct {
Withdrawal *crossdomain.Withdrawal `json:"withdrawal"`
Legacy *crossdomain.LegacyWithdrawal `json:"legacy"`
Trace callFrame `json:"trace"`
Index int `json:"index"`
Reason string `json:"reason"`
}
// findWithdrawalCall will find the call frame for the call that // findWithdrawalCall will find the call frame for the call that
// represents the user's intent. // represents the user's intent.
func findWithdrawalCall(trace *callFrame, wd *crossdomain.LegacyWithdrawal, l1xdm common.Address) *callFrame { func findWithdrawalCall(trace *callFrame, wd *crossdomain.LegacyWithdrawal, l1xdm common.Address) *callFrame {
...@@ -69,8 +86,7 @@ func createOutput( ...@@ -69,8 +86,7 @@ func createOutput(
withdrawal *crossdomain.Withdrawal, withdrawal *crossdomain.Withdrawal,
oracle *bindings.L2OutputOracle, oracle *bindings.L2OutputOracle,
blockNumber *big.Int, blockNumber *big.Int,
l2Client bind.ContractBackend, clients *clients,
l2GethClient *gethclient.Client,
) (*big.Int, bindings.TypesOutputRootProof, [][]byte, error) { ) (*big.Int, bindings.TypesOutputRootProof, [][]byte, error) {
// compute the storage slot that the withdrawal is stored in // compute the storage slot that the withdrawal is stored in
slot, err := withdrawal.StorageSlot() slot, err := withdrawal.StorageSlot()
...@@ -78,7 +94,7 @@ func createOutput( ...@@ -78,7 +94,7 @@ func createOutput(
return nil, bindings.TypesOutputRootProof{}, nil, err return nil, bindings.TypesOutputRootProof{}, nil, err
} }
// find the output index that the withdrawal was commited to in // find the output index that the withdrawal was committed to in
l2OutputIndex, err := oracle.GetL2OutputIndexAfter(&bind.CallOpts{}, blockNumber) l2OutputIndex, err := oracle.GetL2OutputIndexAfter(&bind.CallOpts{}, blockNumber)
if err != nil { if err != nil {
return nil, bindings.TypesOutputRootProof{}, nil, err return nil, bindings.TypesOutputRootProof{}, nil, err
...@@ -98,13 +114,14 @@ func createOutput( ...@@ -98,13 +114,14 @@ func createOutput(
) )
// get the block header committed to in the output // get the block header committed to in the output
header, err := l2Client.HeaderByNumber(context.Background(), l2Output.L2BlockNumber) header, err := clients.L2Client.HeaderByNumber(context.Background(), l2Output.L2BlockNumber)
if err != nil { if err != nil {
return nil, bindings.TypesOutputRootProof{}, nil, err return nil, bindings.TypesOutputRootProof{}, nil, err
} }
// get the storage proof for the withdrawal's storage slot // get the storage proof for the withdrawal's storage slot
proof, err := l2GethClient.GetProof(context.Background(), predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, blockNumber) proof, err := clients.L2GethClient.GetProof(context.Background(), predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, blockNumber)
if err != nil { if err != nil {
return nil, bindings.TypesOutputRootProof{}, nil, err return nil, bindings.TypesOutputRootProof{}, nil, err
} }
...@@ -124,7 +141,6 @@ func createOutput( ...@@ -124,7 +141,6 @@ func createOutput(
LatestBlockhash: header.Hash(), LatestBlockhash: header.Hash(),
} }
// compute a storage root hash locally
localOutputRootHash := crypto.Keccak256Hash( localOutputRootHash := crypto.Keccak256Hash(
outputRootProof.Version[:], outputRootProof.Version[:],
outputRootProof.StateRoot[:], outputRootProof.StateRoot[:],
...@@ -134,7 +150,7 @@ func createOutput( ...@@ -134,7 +150,7 @@ func createOutput(
// ensure that the locally computed hash matches // ensure that the locally computed hash matches
if l2Output.OutputRoot != localOutputRootHash { if l2Output.OutputRoot != localOutputRootHash {
return nil, bindings.TypesOutputRootProof{}, nil, fmt.Errorf("mismatch in output root hashes", "got", localOutputRootHash, "expect", l2Output.OutputRoot) return nil, bindings.TypesOutputRootProof{}, nil, fmt.Errorf("mismatch in output root hashes, got 0x%x expected 0x%x", localOutputRootHash, l2Output.OutputRoot)
} }
log.Info( log.Info(
"output root proof", "output root proof",
...@@ -142,6 +158,7 @@ func createOutput( ...@@ -142,6 +158,7 @@ func createOutput(
"state-root", common.Hash(outputRootProof.StateRoot), "state-root", common.Hash(outputRootProof.StateRoot),
"storage-root", common.Hash(outputRootProof.MessagePasserStorageRoot), "storage-root", common.Hash(outputRootProof.MessagePasserStorageRoot),
"block-hash", common.Hash(outputRootProof.LatestBlockhash), "block-hash", common.Hash(outputRootProof.LatestBlockhash),
"trie-node-count", len(trieNodes),
) )
return l2OutputIndex, outputRootProof, trieNodes, nil return l2OutputIndex, outputRootProof, trieNodes, nil
...@@ -184,10 +201,6 @@ func main() { ...@@ -184,10 +201,6 @@ func main() {
Name: "evm-messages", Name: "evm-messages",
Usage: "Path to evm-messages.json", Usage: "Path to evm-messages.json",
}, },
&cli.Uint64Flag{
Name: "bedrock-transition-block-number",
Usage: "The blocknumber of the bedrock transition block",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "private-key", Name: "private-key",
Usage: "Key to sign transactions with", Usage: "Key to sign transactions with",
...@@ -199,303 +212,212 @@ func main() { ...@@ -199,303 +212,212 @@ func main() {
}, },
}, },
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
// set up the rpc clients clients, err := newClients(ctx)
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return err
}
l1ChainID, err := l1Client.ChainID(context.Background())
if err != nil { if err != nil {
return err return err
} }
log.Info("Set up L1 RPC Client", "chain-id", l1ChainID) // initialize the contract bindings
contracts, err := newContracts(ctx, clients.L1Client, clients.L2Client)
l2RpcURL := ctx.String("l2-rpc-url")
l2Client, err := ethclient.Dial(l2RpcURL)
if err != nil { if err != nil {
return err return err
} }
l2ChainID, err := l2Client.ChainID(context.Background()) l1xdmAddr := common.HexToAddress(ctx.String("l1-crossdomain-messenger-address"))
if err != nil {
return err
}
log.Info("Set up L2 RPC Client", "chain-id", l2ChainID)
l1RpcClient, err := rpc.DialContext(context.Background(), l1RpcURL) l1ChainID, err := clients.L1Client.ChainID(context.Background())
if err != nil { if err != nil {
return err return err
} }
l2RpcClient, err := rpc.DialContext(context.Background(), l2RpcURL) // create the set of withdrawals
wds, err := newWithdrawals(ctx, l1ChainID)
if err != nil { if err != nil {
return err return err
} }
// this script requires geth's rpcs
gclient := gethclient.New(l2RpcClient)
// get the evm and ovm messages witness files used as part of period, err := contracts.OptimismPortal.FINALIZATIONPERIODSECONDS(&bind.CallOpts{})
// migration
ovmMsgs := ctx.String("ovm-messages")
evmMsgs := ctx.String("evm-messages")
log.Debug("Migration data", "ovm-path", ovmMsgs, "evm-messages", evmMsgs)
ovmMessages, err := migration.NewSentMessage(ovmMsgs)
if err != nil {
return err
}
evmMessages, err := migration.NewSentMessage(evmMsgs)
if err != nil { if err != nil {
return err return err
} }
optimismPortalAddress := ctx.String("optimism-portal-address") bedrockStartingBlockNumber, err := contracts.L2OutputOracle.StartingBlockNumber(&bind.CallOpts{})
if len(optimismPortalAddress) == 0 {
return errors.New("OptimismPortal address not configured")
}
optimismPortalAddr := common.HexToAddress(optimismPortalAddress)
migrationData := migration.MigrationData{
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
// create the set of withdrawals
wds, err := migrationData.ToWithdrawals()
if err != nil { if err != nil {
return err return err
} }
if len(wds) == 0 {
return errors.New("no withdrawals")
}
log.Info("Converted migration data to withdrawals successfully", "count", len(wds))
l1xdmAddress := ctx.String("l1-crossdomain-messenger-address") bedrockStartingBlock, err := clients.L2Client.BlockByNumber(context.Background(), bedrockStartingBlockNumber)
if l1xdmAddress == "" {
return errors.New("Must pass in --l1-crossdomain-messenger-address")
}
l1xdmAddr := common.HexToAddress(l1xdmAddress)
l1CrossDomainMessenger, err := bindings.NewL1CrossDomainMessenger(l1xdmAddr, l1Client) log.Info("Withdrawal config", "finalization-period", period, "bedrock-starting-block-number", bedrockStartingBlockNumber, "bedrock-starting-block-hash", bedrockStartingBlock.Hash().Hex())
if err != nil {
return err
}
portal, err := bindings.NewOptimismPortal(optimismPortalAddr, l1Client) if !bytes.Equal(bedrockStartingBlock.Extra(), genesis.BedrockTransitionBlockExtraData) {
if err != nil { return errors.New("genesis block mismatch")
return err
} }
l2OracleAddr, err := portal.L2ORACLE(&bind.CallOpts{}) outfile := ctx.String("bad-withdrawals-out")
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o755)
if err != nil { if err != nil {
return err return err
} }
oracle, err := bindings.NewL2OutputOracle(l2OracleAddr, l1Client)
if err != nil {
return nil
}
log.Info(
"Addresses",
"l1-crossdomain-messenger", l1xdmAddr,
"optimism-portal", optimismPortalAddr,
"output-oracle", l2OracleAddr,
)
period, err := portal.FINALIZATIONPERIODSECONDS(&bind.CallOpts{})
if err != nil {
return err
}
transitionBlockNumber := new(big.Int).SetUint64(ctx.Uint64("bedrock-transition-block-number"))
log.Info("Withdrawal config", "finalization-period", period, "bedrock-transition-block-number", transitionBlockNumber)
if ctx.String("private-key") == "" { // create a transactor
return errors.New("No private key to transact with") opts, err := newTransactor(ctx)
}
privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ctx.String("private-key"), "0x"))
if err != nil { if err != nil {
return err return err
} }
type badWithdrawal struct { // Need this to compare in event parsing
Withdrawal *crossdomain.Withdrawal `json:"withdrawal"` l1StandardBridgeAddress := common.HexToAddress(ctx.String("l1-standard-bridge-address"))
Legacy *crossdomain.LegacyWithdrawal `json:"legacy"`
Trace callFrame `json:"trace"`
Index int `json"index"`
}
badWithdrawals := make([]badWithdrawal, 0)
// iterate over all of the withdrawals and submit them // iterate over all of the withdrawals and submit them
for i, wd := range wds { for i, wd := range wds {
log.Info("Processing withdrawal", "index", i) log.Info("Processing withdrawal", "index", i)
// migrate the withdrawal // migrate the withdrawal
withdrawal, err := crossdomain.MigrateWithdrawal(wd, &l1xdmAddr) withdrawal, err := crossdomain.MigrateWithdrawal(wd, &l1xdmAddr)
if err != nil { if err != nil {
return err return err
} }
// compute the withdrawal hash
// Pass to Portal
hash, err := withdrawal.Hash() hash, err := withdrawal.Hash()
if err != nil { if err != nil {
return err return err
} }
lcdm := wd.CrossDomainMessage()
legacyXdmHash, err := lcdm.Hash()
if err != nil {
return err
}
// check to see if the withdrawal has already been successfully // check to see if the withdrawal has already been successfully
// relayed or received // relayed or received
isSuccess, err := l1CrossDomainMessenger.SuccessfulMessages(&bind.CallOpts{}, hash) isSuccess, err := contracts.L1CrossDomainMessenger.SuccessfulMessages(&bind.CallOpts{}, legacyXdmHash)
if err != nil { if err != nil {
return err return err
} }
isReceived, err := l1CrossDomainMessenger.ReceivedMessages(&bind.CallOpts{}, hash) isFailed, err := contracts.L1CrossDomainMessenger.FailedMessages(&bind.CallOpts{}, legacyXdmHash)
if err != nil { if err != nil {
return err return err
} }
// compute the storage slot
slot, err := withdrawal.StorageSlot() // This is just wrong
xdmHash := crypto.Keccak256Hash(withdrawal.Data)
if err != nil {
return err
}
isSuccessNew, err := contracts.L1CrossDomainMessenger.SuccessfulMessages(&bind.CallOpts{}, xdmHash)
if err != nil {
return err
}
isFailedNew, err := contracts.L1CrossDomainMessenger.FailedMessages(&bind.CallOpts{}, xdmHash)
if err != nil { if err != nil {
return err return err
} }
log.Info("cross domain messenger status", "hash", hash.Hex(), "success", isSuccess, "received", isReceived, "slot", slot.Hex()) log.Info("cross domain messenger status", "hash", legacyXdmHash.Hex(), "success", isSuccess, "failed", isFailed, "is-success-new", isSuccessNew, "is-failed-new", isFailedNew)
// compute the storage slot
slot, err := withdrawal.StorageSlot()
if err != nil {
return err
}
// successful messages can be skipped, received messages failed // successful messages can be skipped, received messages failed
// their execution and should be replayed // their execution and should be replayed
if isSuccess { if isSuccessNew {
log.Info("Message already relayed", "index", i, "hash", hash, "slot", slot) log.Info("Message already relayed", "index", i, "hash", hash, "slot", slot)
continue continue
} }
// create the values required for submitting a proof // check the storage value of the slot to ensure that it is in
l2OutputIndex, outputRootProof, trieNodes, err := createOutput(withdrawal, oracle, transitionBlockNumber, l2Client, gclient) // the L2 storage. Without this check, the proof will fail
storageValue, err := clients.L2Client.StorageAt(context.Background(), predeploys.L2ToL1MessagePasserAddr, slot, nil)
if err != nil { if err != nil {
return err return err
} }
log.Debug("L2ToL1MessagePasser status", "value", common.Bytes2Hex(storageValue))
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, l1ChainID) // the value should be set to a boolean in storage
if !bytes.Equal(storageValue, abiTrue.Bytes()) {
return fmt.Errorf("storage slot %x not found in state", slot)
}
legacySlot, err := wd.StorageSlot()
if err != nil {
return err
}
legacyStorageValue, err := clients.L2Client.StorageAt(context.Background(), predeploys.LegacyMessagePasserAddr, legacySlot, nil)
if err != nil { if err != nil {
return err return err
} }
log.Debug("LegacyMessagePasser status", "value", common.Bytes2Hex(legacyStorageValue))
// check to see if its already been proven // check to see if its already been proven
proven, err := portal.ProvenWithdrawals(&bind.CallOpts{}, hash) proven, err := contracts.OptimismPortal.ProvenWithdrawals(&bind.CallOpts{}, hash)
if err != nil { if err != nil {
return err return err
} }
wdTx := withdrawal.WithdrawalTransaction()
// check to see if its been proven
// if it has not been proven, then prove it // if it has not been proven, then prove it
if proven.Timestamp.Cmp(common.Big0) == 0 { if proven.Timestamp.Cmp(common.Big0) == 0 {
log.Info("Proving withdrawal to OptimismPortal") log.Info("Proving withdrawal to OptimismPortal")
if err := proveWithdrawalTransaction(contracts, clients, opts, withdrawal, bedrockStartingBlockNumber, period); err != nil {
tx, err := portal.ProveWithdrawalTransaction(
opts,
wdTx,
l2OutputIndex,
outputRootProof,
trieNodes,
)
if err != nil {
return err
}
receipt, err := bind.WaitMined(context.Background(), l1Client, tx)
if err != nil {
return err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("withdrawal proof unsuccessful")
}
log.Info("withdrawal proved", "tx-hash", tx.Hash(), "withdrawal-hash", hash)
block, err := l1Client.BlockByHash(context.Background(), receipt.BlockHash)
if err != nil {
return err return err
} }
initialTime := block.Time()
for {
log.Info("waiting for finalization")
if block.Time() >= initialTime+period.Uint64() {
log.Info("can be finalized")
break
}
time.Sleep(1 * time.Second)
block, err = l1Client.BlockByNumber(context.Background(), nil)
if err != nil {
return err
}
}
} else { } else {
log.Info("Withdrawal already proven to OptimismPortal") log.Info("Withdrawal already proven to OptimismPortal")
} }
// check to see if the withdrawal has been finalized already // check to see if the withdrawal has been finalized already
isFinalized, err := portal.FinalizedWithdrawals(&bind.CallOpts{}, hash) isFinalized, err := contracts.OptimismPortal.FinalizedWithdrawals(&bind.CallOpts{}, hash)
if err != nil { if err != nil {
return err return err
} }
if !isFinalized { if !isFinalized {
log.Info("Finalizing withdrawal")
// Get the ETH balance of the withdrawal target *before* the finalization // Get the ETH balance of the withdrawal target *before* the finalization
targetBalBefore, err := l1Client.BalanceAt(context.Background(), common.BytesToAddress(wd.Target.Bytes()), nil) targetBalBefore, err := clients.L1Client.BalanceAt(context.Background(), *wd.Target, nil)
if err != nil { if err != nil {
return err return err
} }
log.Debug("Balance before finalization", "balance", targetBalBefore, "account", *wd.Target)
log.Debug(fmt.Sprintf("Target balance before finalization: %v", targetBalBefore)) log.Info("Finalizing withdrawal")
receipt, err := finalizeWithdrawalTransaction(contracts, clients, opts, wd, withdrawal)
log.Info("withdrawal finalized", "tx-hash", receipt.TxHash, "withdrawal-hash", hash)
// Finalize withdrawal finalizationTrace, err := callTrace(clients, receipt)
tx, err := portal.FinalizeWithdrawalTransaction(
opts,
wdTx,
)
if err != nil { if err != nil {
return err return nil
} }
receipt, err := bind.WaitMined(context.Background(), l1Client, tx) isSuccessNewPost, err := contracts.L1CrossDomainMessenger.SuccessfulMessages(&bind.CallOpts{}, xdmHash)
if err != nil { if err != nil {
return err return err
} }
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("withdrawal finalize unsuccessful")
}
log.Info("withdrawal finalized", "tx-hash", tx.Hash(), "withdrawal-hash", hash) // This would indicate that there is a replayability problem
if isSuccess && isSuccessNewPost {
// fetch the call trace if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "should revert"); err != nil {
var finalizationTrace callFrame return err
tracer := "callTracer" }
traceConfig := tracers.TraceConfig{ panic("THIS SHOULD REVERT")
Tracer: &tracer,
}
err = l1RpcClient.Call(&finalizationTrace, "debug_traceTransaction", receipt.TxHash, traceConfig)
if err != nil {
return err
} }
callFrame := findWithdrawalCall(&finalizationTrace, wd, l1xdmAddr) callFrame := findWithdrawalCall(&finalizationTrace, wd, l1xdmAddr)
if callFrame == nil { if callFrame == nil {
return errors.New("cannot find callframe") if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "cannot find callframe"); err != nil {
return err
}
continue
} }
traceJson, err := json.MarshalIndent(callFrame, "", " ") traceJson, err := json.MarshalIndent(callFrame, "", " ")
if err != nil { if err != nil {
return err return err
} }
log.Info(fmt.Sprintf("%v", string(traceJson))) log.Debug(fmt.Sprintf("%v", string(traceJson)))
erc20Abi, err := bindings.ERC20MetaData.GetAbi()
if err != nil {
return err
}
transferEvent := erc20Abi.Events["Transfer"]
abi, err := bindings.L1StandardBridgeMetaData.GetAbi() abi, err := bindings.L1StandardBridgeMetaData.GetAbi()
if err != nil { if err != nil {
...@@ -517,79 +439,13 @@ func main() { ...@@ -517,79 +439,13 @@ func main() {
switch method.Name { switch method.Name {
case "finalizeERC20Withdrawal": case "finalizeERC20Withdrawal":
// Handle logic for ERC20 withdrawals if err := handleFinalizeERC20Withdrawal(args, receipt, l1StandardBridgeAddress); err != nil {
l1Token, ok := args[0].(common.Address) return err
if !ok {
return fmt.Errorf("invalid abi")
}
l2Token, ok := args[1].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
from, ok := args[2].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
to, ok := args[3].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
amount, ok := args[4].(*big.Int)
if !ok {
return fmt.Errorf("invalid abi")
}
extraData, ok := args[5].([]byte)
if !ok {
return fmt.Errorf("invalid abi")
}
log.Info(
"decoded calldata",
"l1Token", l1Token,
"l2Token", l2Token,
"from", from,
"to", to,
"amount", amount,
"extraData", extraData,
)
// Look for the ERC20 token transfer topic
for _, l := range receipt.Logs {
topic := l.Topics[0]
if topic == transferEvent.ID {
a, _ := transferEvent.Inputs.Unpack(l.Data)
// TODO: add a check here for balance diff
log.Info("EVENT FOUND", "args", a)
}
log.Info("receipt topic", "hex", topic.Hex())
} }
case "finalizeETHWithdrawal": case "finalizeETHWithdrawal":
// handle logic for ETH withdrawals if err := handleFinalizeETHWithdrawal(args); err != nil {
from, ok := args[0].(common.Address) return err
if !ok {
return fmt.Errorf("invalid type: from")
} }
to, ok := args[1].(common.Address)
if !ok {
return fmt.Errorf("invalid type: to")
}
amount, ok := args[2].(*big.Int)
if !ok {
return fmt.Errorf("invalid type: amount")
}
extraData, ok := args[3].([]byte)
if !ok {
return fmt.Errorf("invalid type: extraData")
}
log.Info(
"decoded calldata",
"from", from,
"to", to,
"amount", amount,
"extraData", extraData,
)
default: default:
log.Info("Unhandled method", "name", method.Name) log.Info("Unhandled method", "name", method.Name)
} }
...@@ -604,55 +460,41 @@ func main() { ...@@ -604,55 +460,41 @@ func main() {
log.Info("withdrawal action", "function", method.Name, "value", wdValue) log.Info("withdrawal action", "function", method.Name, "value", wdValue)
} else { } else {
log.Info("unknown method", "to", wd.Target, "data", hexutil.Encode(wd.Data)) log.Info("unknown method", "to", wd.Target, "data", hexutil.Encode(wd.Data))
if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "unknown method"); err != nil {
badWithdrawals = append(badWithdrawals, badWithdrawal{ return err
Withdrawal: withdrawal, }
Legacy: wd,
Trace: finalizationTrace,
Index: i,
})
} }
// check that the user's intents are actually executed // check that the user's intents are actually executed
if common.HexToAddress(callFrame.To) != *wd.Target { if common.HexToAddress(callFrame.To) != *wd.Target {
badWithdrawals = append(badWithdrawals, badWithdrawal{
Withdrawal: withdrawal,
Legacy: wd,
Trace: finalizationTrace,
Index: i,
})
log.Info("target mismatch", "index", i) log.Info("target mismatch", "index", i)
continue if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "target mismatch"); err != nil {
return err
}
panic("target mismatch")
//continue
} }
if !bytes.Equal(hexutil.MustDecode(callFrame.Input), wd.Data) { if !bytes.Equal(hexutil.MustDecode(callFrame.Input), wd.Data) {
badWithdrawals = append(badWithdrawals, badWithdrawal{
Withdrawal: withdrawal,
Legacy: wd,
Trace: finalizationTrace,
Index: i,
})
log.Info("calldata mismatch", "index", i) log.Info("calldata mismatch", "index", i)
continue if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "calldata mismatch"); err != nil {
return err
}
panic("calldata mismatch")
//continue
} }
if callFrame.Value != "0x"+wdValue.Text(16) { if callFrame.BigValue().Cmp(wdValue) != 0 {
badWithdrawals = append(badWithdrawals, badWithdrawal{
Withdrawal: withdrawal,
Legacy: wd,
Trace: finalizationTrace,
Index: i,
})
log.Info("value mismatch", "index", i) log.Info("value mismatch", "index", i)
if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "value mismatch"); err != nil {
continue return err
}
panic("value mismatch")
//continue
} }
// Get the ETH balance of the withdrawal target *after* the finalization // Get the ETH balance of the withdrawal target *after* the finalization
targetBalAfter, err := l1Client.BalanceAt(context.Background(), *wd.Target, nil) targetBalAfter, err := clients.L1Client.BalanceAt(context.Background(), *wd.Target, nil)
if err != nil { if err != nil {
return err return err
} }
...@@ -660,26 +502,23 @@ func main() { ...@@ -660,26 +502,23 @@ func main() {
diff := new(big.Int).Sub(targetBalAfter, targetBalBefore) diff := new(big.Int).Sub(targetBalAfter, targetBalBefore)
log.Debug("balances", "before", targetBalBefore, "after", targetBalAfter, "diff", diff) log.Debug("balances", "before", targetBalBefore, "after", targetBalAfter, "diff", diff)
if diff.Cmp(wdValue) != 0 { isSuccessNewPost, err = contracts.L1CrossDomainMessenger.SuccessfulMessages(&bind.CallOpts{}, xdmHash)
badWithdrawals = append(badWithdrawals, badWithdrawal{ if err != nil {
Withdrawal: withdrawal, return err
Legacy: wd, }
Trace: finalizationTrace,
Index: i,
})
log.Info("native eth balance diff mismatch", "index", i)
continue if diff.Cmp(wdValue) != 0 && isSuccessNewPost && isSuccess {
log.Info("native eth balance diff mismatch", "index", i, "diff", diff, "val", wdValue)
if err := writeSuspicious(f, withdrawal, wd, finalizationTrace, i, "balance mismatch"); err != nil {
return err
}
panic("balance mismatch")
//continue
} }
} else {
log.Info("Already finalized")
} }
} }
// Write the stuff to disk
if err := writeJSON(ctx.String("bad-withdrawals-out"), badWithdrawals); err != nil {
return err
}
return nil return nil
}, },
} }
...@@ -689,14 +528,415 @@ func main() { ...@@ -689,14 +528,415 @@ func main() {
} }
} }
func writeJSON(outfile string, input interface{}) error { func callTrace(c *clients, receipt *types.Receipt) (callFrame, error) {
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) var finalizationTrace callFrame
tracer := "callTracer"
traceConfig := tracers.TraceConfig{
Tracer: &tracer,
}
err := c.L1RpcClient.Call(&finalizationTrace, "debug_traceTransaction", receipt.TxHash, traceConfig)
if err != nil {
return finalizationTrace, err
}
return finalizationTrace, err
}
func handleFinalizeETHWithdrawal(args []any) error {
from, ok := args[0].(common.Address)
if !ok {
return fmt.Errorf("invalid type: from")
}
to, ok := args[1].(common.Address)
if !ok {
return fmt.Errorf("invalid type: to")
}
amount, ok := args[2].(*big.Int)
if !ok {
return fmt.Errorf("invalid type: amount")
}
extraData, ok := args[3].([]byte)
if !ok {
return fmt.Errorf("invalid type: extraData")
}
log.Info(
"decoded calldata",
"from", from,
"to", to,
"amount", amount,
"extraData", extraData,
)
return nil
}
func handleFinalizeERC20Withdrawal(args []any, receipt *types.Receipt, l1StandardBridgeAddress common.Address) error {
erc20Abi, err := bindings.ERC20MetaData.GetAbi()
if err != nil {
return err
}
transferEvent := erc20Abi.Events["Transfer"]
// Handle logic for ERC20 withdrawals
l1Token, ok := args[0].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
l2Token, ok := args[1].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
from, ok := args[2].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
to, ok := args[3].(common.Address)
if !ok {
return fmt.Errorf("invalid abi")
}
amount, ok := args[4].(*big.Int)
if !ok {
return fmt.Errorf("invalid abi")
}
extraData, ok := args[5].([]byte)
if !ok {
return fmt.Errorf("invalid abi")
}
log.Info(
"decoded calldata",
"l1Token", l1Token,
"l2Token", l2Token,
"from", from,
"to", to,
"amount", amount,
"extraData", extraData,
)
// Look for the ERC20 token transfer topic
for _, l := range receipt.Logs {
topic := l.Topics[0]
if topic == transferEvent.ID {
if l.Address == l1Token {
a, _ := transferEvent.Inputs.Unpack(l.Data)
if len(l.Topics) < 3 {
return fmt.Errorf("")
}
fmt.Printf("%#v\n", l.Topics)
_from := common.BytesToAddress(l.Topics[1].Bytes())
_to := common.BytesToAddress(l.Topics[2].Bytes())
// from the L1StandardBridge
if _from != l1StandardBridgeAddress {
return fmt.Errorf("from mismatch: %x - %x", _from, l1StandardBridgeAddress)
}
if to != _to {
return fmt.Errorf("to mismatch: %x - %x", to, _to)
}
_amount, ok := a[0].(*big.Int)
if !ok {
return fmt.Errorf("invalid abi in transfer event")
}
if amount.Cmp(_amount) != 0 {
return fmt.Errorf("amount mismatch: %d - %d", amount, _amount)
}
}
}
}
return nil
}
func proveWithdrawalTransaction(c *contracts, cl *clients, opts *bind.TransactOpts, withdrawal *crossdomain.Withdrawal, bn, finalizationPeriod *big.Int) error {
l2OutputIndex, outputRootProof, trieNodes, err := createOutput(withdrawal, c.L2OutputOracle, bn, cl)
if err != nil {
return err
}
hash, err := withdrawal.Hash()
if err != nil {
return err
}
wdTx := withdrawal.WithdrawalTransaction()
tx, err := c.OptimismPortal.ProveWithdrawalTransaction(
opts,
wdTx,
l2OutputIndex,
outputRootProof,
trieNodes,
)
if err != nil {
return err
}
receipt, err := bind.WaitMined(context.Background(), cl.L1Client, tx)
if err != nil {
return err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return errors.New("withdrawal proof unsuccessful")
}
log.Info("withdrawal proved", "tx-hash", tx.Hash(), "withdrawal-hash", hash)
block, err := cl.L1Client.BlockByHash(context.Background(), receipt.BlockHash)
if err != nil { if err != nil {
return err return err
} }
defer f.Close() initialTime := block.Time()
for {
log.Info("waiting for finalization")
if block.Time() >= initialTime+finalizationPeriod.Uint64() {
log.Info("can be finalized")
break
}
time.Sleep(1 * time.Second)
block, err = cl.L1Client.BlockByNumber(context.Background(), nil)
if err != nil {
return err
}
}
return nil
}
func finalizeWithdrawalTransaction(
c *contracts,
cl *clients,
opts *bind.TransactOpts,
wd *crossdomain.LegacyWithdrawal,
withdrawal *crossdomain.Withdrawal,
) (*types.Receipt, error) {
if wd.Target == nil {
return nil, errors.New("withdrawal target is nil, should never happen")
}
wdTx := withdrawal.WithdrawalTransaction()
// Finalize withdrawal
tx, err := c.OptimismPortal.FinalizeWithdrawalTransaction(
opts,
wdTx,
)
if err != nil {
return nil, err
}
receipt, err := bind.WaitMined(context.Background(), cl.L1Client, tx)
if err != nil {
return nil, err
}
if receipt.Status != types.ReceiptStatusSuccessful {
return nil, errors.New("withdrawal finalize unsuccessful")
}
return receipt, nil
}
// contracts represents a set of bound contracts
type contracts struct {
OptimismPortal *bindings.OptimismPortal
L1CrossDomainMessenger *bindings.L1CrossDomainMessenger
L2OutputOracle *bindings.L2OutputOracle
}
// newContracts will create a contracts struct with the contract bindings
// preconfigured
func newContracts(ctx *cli.Context, l1Backend, l2Backend bind.ContractBackend) (*contracts, error) {
optimismPortalAddress := ctx.String("optimism-portal-address")
if len(optimismPortalAddress) == 0 {
return nil, errors.New("OptimismPortal address not configured")
}
optimismPortalAddr := common.HexToAddress(optimismPortalAddress)
enc := json.NewEncoder(f) portal, err := bindings.NewOptimismPortal(optimismPortalAddr, l1Backend)
enc.SetIndent("", " ") if err != nil {
return enc.Encode(input) return nil, err
}
l1xdmAddress := ctx.String("l1-crossdomain-messenger-address")
if l1xdmAddress == "" {
return nil, errors.New("L1CrossDomainMessenger address not configured")
}
l1xdmAddr := common.HexToAddress(l1xdmAddress)
l1CrossDomainMessenger, err := bindings.NewL1CrossDomainMessenger(l1xdmAddr, l1Backend)
if err != nil {
return nil, err
}
l2OracleAddr, err := portal.L2ORACLE(&bind.CallOpts{})
if err != nil {
return nil, err
}
oracle, err := bindings.NewL2OutputOracle(l2OracleAddr, l1Backend)
if err != nil {
return nil, err
}
log.Info(
"Addresses",
"l1-crossdomain-messenger", l1xdmAddr,
"optimism-portal", optimismPortalAddr,
"l2-output-oracle", l2OracleAddr,
)
return &contracts{
OptimismPortal: portal,
L1CrossDomainMessenger: l1CrossDomainMessenger,
L2OutputOracle: oracle,
}, nil
}
// clients represents a set of initialized RPC clients
type clients struct {
L1Client *ethclient.Client
L2Client *ethclient.Client
L1RpcClient *rpc.Client
L2RpcClient *rpc.Client
L1GethClient *gethclient.Client
L2GethClient *gethclient.Client
}
// newClients will create new RPC clients
func newClients(ctx *cli.Context) (*clients, error) {
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return nil, err
}
l1ChainID, err := l1Client.ChainID(context.Background())
if err != nil {
return nil, err
}
l2RpcURL := ctx.String("l2-rpc-url")
l2Client, err := ethclient.Dial(l2RpcURL)
if err != nil {
return nil, err
}
l2ChainID, err := l2Client.ChainID(context.Background())
if err != nil {
return nil, err
}
l1RpcClient, err := rpc.DialContext(context.Background(), l1RpcURL)
if err != nil {
return nil, err
}
l2RpcClient, err := rpc.DialContext(context.Background(), l2RpcURL)
if err != nil {
return nil, err
}
l1GethClient := gethclient.New(l1RpcClient)
l2GethClient := gethclient.New(l2RpcClient)
log.Info(
"Set up RPC clients",
"l1-chain-id", l1ChainID,
"l2-chain-id", l2ChainID,
)
return &clients{
L1Client: l1Client,
L2Client: l2Client,
L1RpcClient: l1RpcClient,
L2RpcClient: l2RpcClient,
L1GethClient: l1GethClient,
L2GethClient: l2GethClient,
}, nil
}
// newWithdrawals will create a set of legacy withdrawals
func newWithdrawals(ctx *cli.Context, l1ChainID *big.Int) ([]*crossdomain.LegacyWithdrawal, error) {
ovmMsgs := ctx.String("ovm-messages")
evmMsgs := ctx.String("evm-messages")
log.Debug("Migration data", "ovm-path", ovmMsgs, "evm-messages", evmMsgs)
ovmMessages, err := migration.NewSentMessage(ovmMsgs)
if err != nil {
return nil, err
}
// use empty ovmMessages if its not mainnet
if l1ChainID.Cmp(common.Big1) != 0 {
log.Info("not using ovm messages because its not mainnet")
ovmMessages = []*migration.SentMessage{}
}
evmMessages, err := migration.NewSentMessage(evmMsgs)
if err != nil {
return nil, err
}
migrationData := migration.MigrationData{
OvmMessages: ovmMessages,
EvmMessages: evmMessages,
}
wds, err := migrationData.ToWithdrawals()
if err != nil {
return nil, err
}
if len(wds) == 0 {
return nil, errors.New("no withdrawals")
}
log.Info("Converted migration data to withdrawals successfully", "count", len(wds))
return wds, nil
}
// newTransactor creates a new transact context given a cli context
func newTransactor(ctx *cli.Context) (*bind.TransactOpts, error) {
if ctx.String("private-key") == "" {
return nil, errors.New("No private key to transact with")
}
privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(ctx.String("private-key"), "0x"))
if err != nil {
return nil, err
}
l1RpcURL := ctx.String("l1-rpc-url")
l1Client, err := ethclient.Dial(l1RpcURL)
if err != nil {
return nil, err
}
l1ChainID, err := l1Client.ChainID(context.Background())
if err != nil {
return nil, err
}
opts, err := bind.NewKeyedTransactorWithChainID(privateKey, l1ChainID)
if err != nil {
return nil, err
}
return opts, nil
}
func writeSuspicious(
f *os.File,
withdrawal *crossdomain.Withdrawal,
wd *crossdomain.LegacyWithdrawal,
finalizationTrace callFrame,
i int,
reason string,
) error {
bad := suspiciousWithdrawal{
Withdrawal: withdrawal,
Legacy: wd,
Trace: finalizationTrace,
Index: i,
Reason: reason,
}
data, err := json.Marshal(bad)
if err != nil {
return err
}
_, err = f.WriteString(string(data) + "\n")
return err
} }
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"github.com/ethereum-optimism/optimism/op-bindings/predeploys" "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/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
) )
...@@ -17,7 +18,7 @@ import ( ...@@ -17,7 +18,7 @@ import (
type LegacyWithdrawal struct { type LegacyWithdrawal struct {
Target *common.Address `json:"target"` Target *common.Address `json:"target"`
Sender *common.Address `json:"sender"` Sender *common.Address `json:"sender"`
Data []byte `json:"data"` Data hexutil.Bytes `json:"data"`
Nonce *big.Int `json:"nonce"` Nonce *big.Int `json:"nonce"`
} }
...@@ -38,7 +39,7 @@ func NewLegacyWithdrawal(target, sender *common.Address, data []byte, nonce *big ...@@ -38,7 +39,7 @@ func NewLegacyWithdrawal(target, sender *common.Address, data []byte, nonce *big
// through the standard optimism cross domain messaging system by hashing in // through the standard optimism cross domain messaging system by hashing in
// the L2CrossDomainMessenger address. // the L2CrossDomainMessenger address.
func (w *LegacyWithdrawal) Encode() ([]byte, error) { func (w *LegacyWithdrawal) Encode() ([]byte, error) {
enc, err := EncodeCrossDomainMessageV0(w.Target, w.Sender, w.Data, w.Nonce) enc, err := EncodeCrossDomainMessageV0(w.Target, w.Sender, []byte(w.Data), w.Nonce)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot encode LegacyWithdrawal: %w", err) return nil, fmt.Errorf("cannot encode LegacyWithdrawal: %w", err)
} }
...@@ -98,7 +99,7 @@ func (w *LegacyWithdrawal) Decode(data []byte) error { ...@@ -98,7 +99,7 @@ func (w *LegacyWithdrawal) Decode(data []byte) error {
w.Target = &target w.Target = &target
w.Sender = &sender w.Sender = &sender
w.Data = msgData w.Data = hexutil.Bytes(msgData)
w.Nonce = nonce w.Nonce = nonce
return nil return nil
} }
......
...@@ -72,6 +72,12 @@ func (c *CrossDomainMessage) Hash() (common.Hash, error) { ...@@ -72,6 +72,12 @@ func (c *CrossDomainMessage) Hash() (common.Hash, error) {
} }
} }
// HashV1 forces using the V1 hash even if its a legacy hash. This is used
// for the migration process.
func (c *CrossDomainMessage) HashV1() (common.Hash, error) {
return HashCrossDomainMessageV1(c.Nonce, c.Sender, c.Target, c.Value, c.GasLimit, c.Data)
}
// ToWithdrawal will turn a CrossDomainMessage into a Withdrawal. // ToWithdrawal will turn a CrossDomainMessage into a Withdrawal.
// This only works for version 0 CrossDomainMessages as not all of // This only works for version 0 CrossDomainMessages as not all of
// the data is present for version 1 CrossDomainMessages to be turned // the data is present for version 1 CrossDomainMessages to be turned
......
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