diff --git a/op-wheel/cheat/cheat.go b/op-wheel/cheat/cheat.go new file mode 100644 index 00000000000..a6603da6963 --- /dev/null +++ b/op-wheel/cheat/cheat.go @@ -0,0 +1,340 @@ +package cheat + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "math/big" + "path/filepath" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie" +) + +var HundredETH = big.NewInt(0).Mul(big.NewInt(100), big.NewInt(1000000000000000000)) + +type Cheater struct { + // The database of the chain with the head block that we patch the state-root of, once the state is updated. + DB ethdb.Database + // Initialized chain, wrapping the database with in-memory presentation of headers and recent changes and such. + Blockchain *core.BlockChain + // The Cheater avoids making writes if this is set to True, and opens the DB as readonly. + ReadOnly bool +} + +func OpenGethRawDB(dataDirPath string, readOnly bool) (ethdb.Database, error) { + // don't use readonly mode in actual DB, it doesn't work with Geth. + db, err := rawdb.NewLevelDBDatabaseWithFreezer(dataDirPath, 2048, 500, filepath.Join(dataDirPath, "ancient"), "", readOnly) + if err != nil { + return nil, fmt.Errorf("failed to open leveldb: %w", err) + } + return db, nil +} + +// OpenGethDB opens a geth database to apply cheats to. +func OpenGethDB(dataDirPath string, readOnly bool) (*Cheater, error) { + db, err := OpenGethRawDB(dataDirPath, readOnly) + if err != nil { + return nil, err + } + ch, err := core.NewBlockChain(db, nil, nil, nil, + beacon.New(ethash.NewFullFaker()), vm.Config{}, nil, nil) + if err != nil { + _ = db.Close() + return nil, fmt.Errorf("failed to open blockchain around chain db: %w", err) + } + return &Cheater{ + DB: db, + Blockchain: ch, + ReadOnly: readOnly, + }, nil +} + +func (ch *Cheater) Close() error { + return ch.DB.Close() +} + +type HeadFn func(headState *state.StateDB) error + +// RunAndClose runs the given function on the head-state, and then persists any changes (if not ReadOnly), +// and updates the blockchain headers indexes to reflect the new state-root, so geth will believe the cheat +// (unless it ever re-applies the block). +func (ch *Cheater) RunAndClose(fn HeadFn) error { + preBlock := ch.Blockchain.CurrentBlock() + if a, b := preBlock.NumberU64(), ch.Blockchain.Genesis().NumberU64(); a <= b { + return fmt.Errorf("cheating at genesis (head block %d <= genesis block %d) is not supported", a, b) + } + state, err := ch.Blockchain.StateAt(preBlock.Root()) + if err != nil { + _ = ch.Close() + return fmt.Errorf("failed to look up head state: %w", err) + } + if err := fn(state); err != nil { + _ = ch.Close() + return fmt.Errorf("failed to run state change: %w", err) + } + if ch.ReadOnly { + return ch.Close() + } + + // commit the changes, and then update the state-root + stateRoot, err := state.Commit(true) + if err != nil { + _ = ch.Close() + return fmt.Errorf("failed to commit state change: %w", err) + } + header := preBlock.Header() + header.Root = stateRoot + blockHash := header.Hash() + + // We have to manually commit the updated state root to the database. + if err := state.Database().TrieDB().Commit(stateRoot, true, nil); err != nil { + return fmt.Errorf("error committing trie db: %w", err) + } + + // based on core.BlockChain.writeHeadBlock: + // Add the block to the canonical chain number scheme and mark as the head + batch := ch.DB.NewBatch() + if ch.Blockchain.CurrentFinalizedBlock().Hash() == preBlock.Hash() { + rawdb.WriteFinalizedBlockHash(batch, blockHash) + } + rawdb.DeleteHeaderNumber(batch, preBlock.Hash()) + rawdb.WriteHeadHeaderHash(batch, blockHash) + rawdb.WriteHeadFastBlockHash(batch, blockHash) + rawdb.WriteCanonicalHash(batch, blockHash, preBlock.NumberU64()) + rawdb.WriteHeaderNumber(batch, blockHash, preBlock.NumberU64()) + rawdb.WriteHeader(batch, header) + // not keyed by blockhash, and we didn't remove any txs, so we just leave this one as-is. + // rawdb.WriteTxLookupEntriesByBlock(batch, block) + rawdb.WriteHeadBlockHash(batch, blockHash) + + // Geth stores the TD for each block separately from the block itself. We must update this + // manually, otherwise Geth thinks we haven't reached TTD yet and tries to build a block + // using Clique consensus, which causes a panic. + rawdb.WriteTd(batch, blockHash, preBlock.NumberU64(), ch.Blockchain.GetTd(preBlock.Hash(), preBlock.NumberU64())) + + // Geth maintains an internal mapping between block bodies and their hashes. None of the database + // accessors above update this mapping, so we need to do it manually. + oldKey := blockBodyKey(preBlock.NumberU64(), preBlock.Hash()) + oldBody := rawdb.ReadBodyRLP(ch.DB, preBlock.Hash(), preBlock.NumberU64()) + newKey := blockBodyKey(preBlock.NumberU64(), blockHash) + if err := batch.Delete(oldKey); err != nil { + return fmt.Errorf("error deleting old block body key") + } + if err := batch.Put(newKey, oldBody); err != nil { + return fmt.Errorf("error setting new block body key") + } + + // Flush the whole batch into the disk, exit the node if failed + if err := batch.Write(); err != nil { + _ = ch.Close() + return fmt.Errorf("failed to update chain indexes and markers: %w", err) + } + // Technically there are more in-memory things to update in real geth, + // to which we don't even have public API access, but that's fine, we're done. + // *And we did update the finalized marker, which is flushed from memory to disk on shutdown in geth. + // bc.hc.SetCurrentHeader(block.Header()) + // headFastBlockGauge.Update(int64(block.NumberU64())) + // headBlockGauge.Update(int64(block.NumberU64())) + + return ch.Close() +} + +// StorageSet modifies the storage of the given address at the given key to the given value. +func StorageSet(address common.Address, key common.Hash, value common.Hash) HeadFn { + return func(headState *state.StateDB) error { + headState.SetState(address, key, value) + return nil + } +} + +// StorageGet just reads the storage of the given address at the given key. +func StorageGet(address common.Address, key common.Hash, w io.Writer) HeadFn { + return func(headState *state.StateDB) error { + value := headState.GetState(address, key) + _, err := io.WriteString(w, value.Hex()) + return err + } +} + +// StorageReadAll reads all values of the given address, and writes it as a (+) diff to the given output writer. +// Simply replace the (+) with (-) if you need to apply the diff as removal of values. +// Combined with StoragePatch this allows for quick surgery of 1 account in one database, +// to another account (maybe even in a different database!). +func StorageReadAll(address common.Address, w io.Writer) HeadFn { + return func(headState *state.StateDB) error { + storage := headState.StorageTrie(address) + iter := trie.NewIterator(storage.NodeIterator(nil)) + for iter.Next() { + if _, err := fmt.Fprintf(w, "+ %x = %x\n", iter.Key, dbValueToHash(iter.Value)); err != nil { + return err + } + } + return nil + } +} + +func dbValueToHash(enc []byte) common.Hash { + var value common.Hash + if len(enc) > 0 { + _, content, _, err := rlp.Split(enc) + if err != nil { + panic(err) + } + value.SetBytes(content) + } + return value +} + +// StorageDiff compares the storage of two different accounts, and writes a patch with differences. +// Each difference is expressed with 1 character + or - to indicate the change from a to b, followed by key = value. +func StorageDiff(out io.Writer, addressA, addressB common.Address) HeadFn { + return func(headState *state.StateDB) error { + aStorage := headState.StorageTrie(addressA) + bStorage := headState.StorageTrie(addressB) + aIter := trie.NewIterator(aStorage.NodeIterator(nil)) + bIter := trie.NewIterator(bStorage.NodeIterator(nil)) + hasA := aIter.Next() + hasB := bIter.Next() + for { + if !hasA && !hasB { + break + } + if cmp := bytes.Compare(aIter.Key, bIter.Key); cmp < 0 { + // a is smaller, and thus missing in b. Print and move forward a + if _, err := fmt.Fprintf(out, "- %x = %x\n", aIter.Key, dbValueToHash(aIter.Value)); err != nil { + return err + } + hasA = aIter.Next() + } else if cmp > 0 { + // b is smaller, and thus missing in a. Print and move forward b + if _, err := fmt.Fprintf(out, "+ %x = %x\n", bIter.Key, dbValueToHash(bIter.Value)); err != nil { + return err + } + hasB = bIter.Next() + } else if cmp == 0 { + // same key, now check if the values differ + if !bytes.Equal(aIter.Value, bIter.Value) { + if _, err := fmt.Fprintf(out, "- %x = %x\n", aIter.Key, dbValueToHash(aIter.Value)); err != nil { + return err + } + if _, err := fmt.Fprintf(out, "+ %x = %x\n", bIter.Key, dbValueToHash(bIter.Value)); err != nil { + return err + } + } + // move both + hasA = aIter.Next() + hasB = bIter.Next() + } + } + return nil + } +} + +// StoragePatch applies a patch of changes to the given state account trie. +// Changes are hex encoded key-value pairs separated by (=). +// Additions are prefixed with (+). +// Deletions are prefixed with (-) and overwrite it to a zero value. +// Comments (#) and empty lines are ignored. +func StoragePatch(patch io.Reader, address common.Address) HeadFn { + return func(headState *state.StateDB) error { + s := bufio.NewScanner(patch) + i := 0 + for s.Scan() { + line := s.Text() + if len(line) < 1 || line[0] == '#' { // skip empty lines and comments + continue + } + parts := strings.Split(line[1:], "=") + keyHex := strings.TrimSpace(parts[0]) + valueHex := strings.TrimSpace(parts[1]) + var key, value common.Hash + if err := key.UnmarshalText([]byte(keyHex)); err != nil { + return fmt.Errorf("key %s is malformatted: %w", keyHex, err) + } + if err := value.UnmarshalText([]byte(valueHex)); err != nil { + return fmt.Errorf("key %s has malformatted value %s: %w", keyHex, valueHex, err) + } + switch line[0] { + case '+': + headState.SetState(address, key, value) + case '-': + headState.SetState(address, key, common.Hash{}) + default: + return fmt.Errorf("unrecognized line diff token") + } + i += 1 + if i%1000 == 0 { // for every 1000 values, commit to disk + if _, err := headState.Commit(true); err != nil { + return fmt.Errorf("failed to commit state to disk after patching %d entries: %w", i, err) + } + } + } + return nil + } +} + +type OvmOwnersConfig struct { + Owner common.Address `json:"owner"` + Sequencer common.Address `json:"sequencer"` + Proposer common.Address `json:"proposer"` +} + +func OvmOwners(conf *OvmOwnersConfig) HeadFn { + return func(headState *state.StateDB) error { + // Address manager owner + headState.SetState(common.HexToAddress("0xa6f73589243a6A7a9023b1Fa0651b1d89c177111"), common.Hash{}, conf.Owner.Hash()) + // L1SB proxy owner + headState.SetState(common.HexToAddress("0x636Af16bf2f682dD3109e60102b8E1A089FedAa8"), common.HexToHash("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"), conf.Owner.Hash()) + // L1XDM owner + headState.SetState(common.HexToAddress("0x5086d1eEF304eb5284A0f6720f79403b4e9bE294"), common.Hash{31: 0x33}, conf.Owner.Hash()) + // L1 ERC721 bridge owner + headState.SetState(common.HexToAddress("0x8DD330DdE8D9898d43b4dc840Da27A07dF91b3c9"), common.HexToHash("0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"), conf.Owner.Hash()) + // Legacy sequencer/proposer addresses + headState.SetState(common.HexToAddress("0xa6f73589243a6A7a9023b1Fa0651b1d89c177111"), common.HexToHash("0x2e0dfce60e9e27f035ce28f63c1bdd77cff6b13d8909da4d81d623ff9123fbdc"), conf.Sequencer.Hash()) + headState.SetState(common.HexToAddress("0xa6f73589243a6A7a9023b1Fa0651b1d89c177111"), common.HexToHash("0x9776dbdebd0d5eedaea450b21da9901ecd5254e5136a3a9b7b0ecd532734d5b5"), conf.Proposer.Hash()) + // Fund sequencer and proposer with 100 ETH + headState.SetBalance(conf.Sequencer, HundredETH) + headState.SetBalance(conf.Proposer, HundredETH) + return nil + } +} + +func SetBalance(addr common.Address, amount *big.Int) HeadFn { + return func(headState *state.StateDB) error { + headState.SetBalance(addr, amount) + return nil + } +} + +func SetNonce(addr common.Address, nonce uint64) HeadFn { + return func(headState *state.StateDB) error { + headState.SetNonce(addr, nonce) + return nil + } +} + +// blockBodyKey returns the database key to use for storing the body of a block. +// This function was copied from Geth's core/rawdb/accessors_chain.go. +func blockBodyKey(number uint64, hash common.Hash) []byte { + return append(append([]byte("b"), encodeBlockNumber(number)...), hash.Bytes()...) +} + +// encodeBlockNumber encodes a block number as big endian uint64. This function was +// copied from Geth's core/rawdb/accessors_chain.go file. +func encodeBlockNumber(number uint64) []byte { + enc := make([]byte, 8) + binary.BigEndian.PutUint64(enc, number) + return enc +}