diff --git a/cmd/hivechain/README.md b/cmd/hivechain/README.md new file mode 100644 index 0000000000..43c862ac72 --- /dev/null +++ b/cmd/hivechain/README.md @@ -0,0 +1,68 @@ +# hivechain + +Hivechain creates a non-empty blockchain for testing purposes. To facilitate good tests, +the created chain excercises many protocol features, including: + +- different types of transactions +- diverse set of contracts with interesting storage, code, etc. +- contracts to create known log events +- non-transaction chain modifications: coinbase fee payments, uncles, withdrawals... + +## Running hivechain + +Here is an example command line invocation of the tool: + + hivechain generate -fork-interval 6 -tx-interval 1 -length 500 -outdir chain -outputs genesis,chain,headfcu + +The command creates a 500-block chain where a new fork gets enabled every six blocks, and +every block contains one 'modification' (i.e. transaction). A number of output files will +be created in the `chain/` directory: + +- `genesis.json` contains the genesis block specification +- `chain.rlp` has the blocks in binary RLP format +- `headfcu.json` contains an Engine API request that sends the head block to a client + +To see all generator options, run: + + hivechain generate -help + +## -outputs + +Different kinds of output files can be created based on the generated chain. The available +output formats are documented below. + +### accounts + +Creates `accounts.json` containing accounts and corresponding private keys. + +### chain, powchain + +`chain` creates `chain.rlp` containing the chain blocks. + +`powchain` creates `powchain.rlp` containing only the pre-merge blocks. + +### fcu, headfcu, newpayload + +`fcu.json` is a JSON array of forkchoiceUpdated requests for all post-merge blocks. + +`headfcu.json` is a request for just the head block. This is useful for triggering a sync in the client. + +`newpayload.json` is a JSON array of newPayload requests for post-merge blocks. + +### genesis + +This writes the `genesis.json` file containing a go-ethereum style genesis spec. Note +this file includes the fork block numbers/timestamps. + +### headblock + +This creates `headblock.json` with a dump of the head header. + +### headstate + +This writes `headstate.json`, a dump of the complete state of the head block. + +### txinfo + +The `txinfo.json` file contains an object with a key for each block modifier, and the +value being information about the activity of the modifier. diff --git a/cmd/hivechain/accounts.go b/cmd/hivechain/accounts.go new file mode 100644 index 0000000000..8d995aef0b --- /dev/null +++ b/cmd/hivechain/accounts.go @@ -0,0 +1,99 @@ +package main + +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +var knownAccounts = []genAccount{ + { + key: mustParseKey("4552dbe6ca4699322b5d923d0c9bcdd24644f5db8bf89a085b67c6c49b8a1b91"), + addr: common.HexToAddress("0x7435ed30A8b4AEb0877CEf0c6E8cFFe834eb865f"), + }, + { + key: mustParseKey("f6a8f1603b8368f3ca373292b7310c53bec7b508aecacd442554ebc1c5d0c856"), + addr: common.HexToAddress("0x84E75c28348fB86AceA1A93a39426d7D60f4CC46"), + }, + { + key: mustParseKey("6e1e16a9c15641c73bf6e237f9293ab1d4e7c12b9adf83cfc94bcf969670f72d"), + addr: common.HexToAddress("0x4ddE844b71bcdf95512Fb4Dc94e84FB67b512eD8"), + }, + { + key: mustParseKey("fc39d1c9ddbba176d806ebb42d7460189fe56ca163ad3eb6143bfc6beb6f6f72"), + addr: common.HexToAddress("0xd803681E487E6AC18053aFc5a6cD813c86Ec3E4D"), + }, + { + key: mustParseKey("a88293fefc623644969e2ce6919fb0dbd0fd64f640293b4bf7e1a81c97e7fc7f"), + addr: common.HexToAddress("0x4a0f1452281bCec5bd90c3dce6162a5995bfe9df"), + }, + { + key: mustParseKey("457075f6822ac29481154792f65c5f1ec335b4fea9ca20f3fea8fa1d78a12c68"), + addr: common.HexToAddress("0x14e46043e63D0E3cdcf2530519f4cFAf35058Cb2"), + }, + { + key: mustParseKey("9ee3fd550664b246ad7cdba07162dd25530a3b1d51476dd1d85bbc29f0592684"), + addr: common.HexToAddress("0xE7d13f7Aa2A838D24c59b40186a0aCa1e21CffCC"), + }, + { + key: mustParseKey("865898edcf43206d138c93f1bbd86311f4657b057658558888aa5ac4309626a6"), + addr: common.HexToAddress("0x16c57eDF7Fa9D9525378B0b81Bf8A3cEd0620C1c"), + }, + { + key: mustParseKey("19168cd7767604b3d19b99dc3da1302b9ccb6ee9ad61660859e07acd4a2625dd"), + addr: common.HexToAddress("0x2D389075BE5be9F2246Ad654cE152cF05990b209"), + }, + { + key: mustParseKey("ee7f7875d826d7443ccc5c174e38b2c436095018774248a8074ee92d8914dcdb"), + addr: common.HexToAddress("0x1F4924B14F34e24159387C0A4CdBaa32f3DDb0cF"), + }, + { + key: mustParseKey("bfcd0e032489319f4e5ca03e643b2025db624be6cf99cbfed90c4502e3754850"), + addr: common.HexToAddress("0x0c2c51a0990AeE1d73C1228de158688341557508"), + }, + { + key: mustParseKey("41be4e00aac79f7ffbb3455053ec05e971645440d594c047cdcc56a3c7458bd6"), + addr: common.HexToAddress("0x5f552da00dFB4d3749D9e62dCeE3c918855A86A0"), + }, + { + key: mustParseKey("71aa7d299c7607dabfc3d0e5213d612b5e4a97455b596c2f642daac43fa5eeaa"), + addr: common.HexToAddress("0x3aE75c08b4c907EB63a8960c45B86E1e9ab6123c"), + }, + { + key: mustParseKey("c825f31cd8792851e33a290b3d749e553983111fc1f36dfbbdb45f101973f6a9"), + addr: common.HexToAddress("0x654aa64f5FbEFb84c270eC74211B81cA8C44A72e"), + }, + { + key: mustParseKey("8d0faa04ae0f9bc3cd4c890aa025d5f40916f4729538b19471c0beefe11d9e19"), + addr: common.HexToAddress("0x717f8AA2b982BeE0e29f573D31Df288663e1Ce16"), + }, + { + key: mustParseKey("47f666f20e2175606355acec0ea1b37870c15e5797e962340da7ad7972a537e8"), + addr: common.HexToAddress("0x4340Ee1b812ACB40a1eb561C019c327b243b92Df"), + }, + { + key: mustParseKey("8d56bcbcf2c1b7109e1396a28d7a0234e33544ade74ea32c460ce4a443b239b1"), + addr: common.HexToAddress("0xC7B99a164Efd027a93f147376Cc7DA7C67c6bbE0"), + }, + { + key: mustParseKey("34391cbbf06956bb506f45ec179cdd84df526aa364e27bbde65db9c15d866d00"), + addr: common.HexToAddress("0x83C7e323d189f18725ac510004fdC2941F8C4A78"), + }, + { + key: mustParseKey("25e6ce8611cefb5cd338aeaa9292ed2139714668d123a4fb156cabb42051b5b7"), + addr: common.HexToAddress("0x1F5BDe34B4afC686f136c7a3CB6EC376F7357759"), + }, + { + key: mustParseKey("14cdde09d1640eb8c3cda063891b0453073f57719583381ff78811efa6d4199f"), + addr: common.HexToAddress("0xedA8645bA6948855E3B3cD596bbB07596d59c603"), + }, +} + +func mustParseKey(s string) *ecdsa.PrivateKey { + key, err := crypto.HexToECDSA(s) + if err != nil { + panic(err) + } + return key +} diff --git a/cmd/hivechain/bytecode/callenv.bin b/cmd/hivechain/bytecode/callenv.bin new file mode 100644 index 0000000000..6ed4b4413a Binary files /dev/null and b/cmd/hivechain/bytecode/callenv.bin differ diff --git a/cmd/hivechain/bytecode/callme.bin b/cmd/hivechain/bytecode/callme.bin new file mode 100644 index 0000000000..ebddffb951 Binary files /dev/null and b/cmd/hivechain/bytecode/callme.bin differ diff --git a/cmd/hivechain/bytecode/callrevert.bin b/cmd/hivechain/bytecode/callrevert.bin new file mode 100644 index 0000000000..5468f315f5 Binary files /dev/null and b/cmd/hivechain/bytecode/callrevert.bin differ diff --git a/cmd/hivechain/bytecode/deployer.bin b/cmd/hivechain/bytecode/deployer.bin new file mode 100644 index 0000000000..89cc8aa4d6 Binary files /dev/null and b/cmd/hivechain/bytecode/deployer.bin differ diff --git a/cmd/hivechain/bytecode/gencode.bin b/cmd/hivechain/bytecode/gencode.bin new file mode 100644 index 0000000000..bc95df8500 Binary files /dev/null and b/cmd/hivechain/bytecode/gencode.bin differ diff --git a/cmd/hivechain/bytecode/genlogs.bin b/cmd/hivechain/bytecode/genlogs.bin new file mode 100644 index 0000000000..c4ae16c800 Binary files /dev/null and b/cmd/hivechain/bytecode/genlogs.bin differ diff --git a/cmd/hivechain/bytecode/genstorage.bin b/cmd/hivechain/bytecode/genstorage.bin new file mode 100644 index 0000000000..69d1f05be9 --- /dev/null +++ b/cmd/hivechain/bytecode/genstorage.bin @@ -0,0 +1 @@ +C[€€U`Zaa¨`W \ No newline at end of file diff --git a/cmd/hivechain/compile.sh b/cmd/hivechain/compile.sh new file mode 100755 index 0000000000..12a9efc464 --- /dev/null +++ b/cmd/hivechain/compile.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -xe + +geas -bin -no-push0 contracts/deployer.eas > bytecode/deployer.bin +geas -bin -no-push0 contracts/callenv.eas > bytecode/callenv.bin +geas -bin -no-push0 contracts/callme.eas > bytecode/callme.bin +geas -bin -no-push0 contracts/callrevert.eas > bytecode/callrevert.bin +geas -bin -no-push0 contracts/genlogs.eas > bytecode/genlogs.bin +geas -bin -no-push0 contracts/gencode.eas > bytecode/gencode.bin +geas -bin -no-push0 contracts/genstorage.eas > bytecode/genstorage.bin diff --git a/cmd/hivechain/contracts.go b/cmd/hivechain/contracts.go new file mode 100644 index 0000000000..94ad377f3b --- /dev/null +++ b/cmd/hivechain/contracts.go @@ -0,0 +1,26 @@ +package main + +import _ "embed" + +//go:embed bytecode/gencode.bin +var gencodeCode []byte + +//go:embed bytecode/genlogs.bin +var genlogsCode []byte + +//go:embed bytecode/genstorage.bin +var genstorageCode []byte + +//go:embed bytecode/deployer.bin +var deployerCode []byte + +//go:embed bytecode/callme.bin +var callmeCode []byte + +//go:embed bytecode/callenv.bin +var callenvCode []byte + +// //go:embed bytecode/deposit.bin +// var depositCode []byte +// +// const depositContractAddr = "0x00000000219ab540356cBB839Cbe05303d7705Fa" diff --git a/cmd/hivechain/contracts/callenv.eas b/cmd/hivechain/contracts/callenv.eas new file mode 100644 index 0000000000..535f69a8a5 --- /dev/null +++ b/cmd/hivechain/contracts/callenv.eas @@ -0,0 +1,36 @@ +;;; -*- mode: asm -*- +;;; This contract returns EVM environment data. + +#define %store { ; [value, ptr] + dup2 ; [ptr, value, ptr] + mstore ; [ptr] + push 32 ; [32, ptr] + add ; [newptr] +} + +.start: + push 0 ; [ptr] + + number ; [v, ptr] + %store ; [ptr] + + chainid ; [v, ptr] + %store ; [ptr] + + coinbase ; [v, ptr] + %store ; [ptr] + + basefee ; [v, ptr] + %store ; [ptr] + + difficulty ; [v, ptr] + %store ; [ptr] + + origin ; [v, ptr] + %store ; [ptr] + + callvalue ; [v, ptr] + %store ; [ptr] + + push 0 ; [offset, ptr] + return ; [] diff --git a/cmd/hivechain/contracts/callme.eas b/cmd/hivechain/contracts/callme.eas new file mode 100644 index 0000000000..5fc04060fa --- /dev/null +++ b/cmd/hivechain/contracts/callme.eas @@ -0,0 +1,40 @@ +;;; -*- mode: asm -*- +;;; This contract returns value `theOutput` if calldata is exactly equal to `theInput`. +;;; Otherwise, reverts with an error message. + +#define theInput 0xff01 +#define theOutput 0xffee + +#define %revert(error) { ; [] + push $error ; [value] + push 0 ; [offset, value] + mstore ; [] + push .bytelen($error) ; [size] + push 32-.bytelen($error) ; [offset, size] + revert +} + +.start: + calldatasize ; [size] + push .bytelen(theInput) ; [exp, iszero] + eq ; [size==exp] + jumpi @compare ; [] + %revert("wrong-calldatasize") + +compare: + push 0 ; [offset] + calldataload ; [calldata] + push 256-.bitlen(theInput) ; [shft, calldata] + shr ; [data] + push theInput ; [expv, data] + eq ; [data==expv] + jumpi @return ; [] + %revert("wrong-calldata") + +return: + push theOutput ; [v] + push 0 ; [offset, v] + mstore ; [] + push .bytelen(theOutput) ; [size] + push 32-.bytelen(theInput) ; [offset, size] + return ; [] diff --git a/cmd/hivechain/contracts/callrevert.eas b/cmd/hivechain/contracts/callrevert.eas new file mode 100644 index 0000000000..94386d94ea --- /dev/null +++ b/cmd/hivechain/contracts/callrevert.eas @@ -0,0 +1,62 @@ +;;; -*- mode: asm -*- +;;; This contract is for testing two common Solidity revert encodings: +;;; panic(uint) and error(string). + +;;; Dispatch +.start: + push 0 ; [offset] + calldataload ; [word] + + ;; if word == 0 revert with panic + iszero ; [word==0] + iszero ; [word!=0] + jumpi @error ; [word] + +#define s_panic .selector("panic(uint)") +#define panicv 17 + +;;; Solidity ABI `panic(17)` +;;; Revert data layout: +;;; +;;; selector :: 4 || value :: 32 +;;; +.panic: + push s_panic << (28*8) ; [sel] + push 0 ; [offset, sel] + mstore ; [] + push 17 ; [panicv] + push 4 ; [offset, panicv] + mstore ; [] + + push 36 ; [length] + push 0 ; [offset, length] + revert ; [] + + +#define s_error .selector("error(string)") +#define errmsg "user error" +#define errmsg_word errmsg << (255-.bitlen(errmsg)) + +;;; Solidity ABI error +;;; +;;; Revert data layout: +;;; +;;; selector :: 4 || 0x20 :: 32 || len :: 32 || data :: len +;;; +error: + push s_error << (28*8) ; [sel] + push 0 ; [offset, sel] + mstore ; [] + push 0x20 ; [ptr] + push 4 ; [offset, ptr] + mstore ; [] + push .bytelen(errmsg) ; [len] + push 36 ; [offset, len] + mstore ; [] + push errmsg_word ; [data] + push 68 ; [offset, data] + mstore ; [] + + push 68 + .bytelen(errmsg) ; [length] + push 0 ; [offset, length] + revert diff --git a/cmd/hivechain/contracts/deployer.eas b/cmd/hivechain/contracts/deployer.eas new file mode 100644 index 0000000000..90e62b81de --- /dev/null +++ b/cmd/hivechain/contracts/deployer.eas @@ -0,0 +1,15 @@ +;;; -*- mode: asm -*- +;;; puts any data after @.end into new contract + + push @.end ; [end] + codesize ; [codesize, end] + sub ; [size] + ;; copy to memory + dup1 ; [size, size] + push @.end ; [offset, size, size] + push 0 ; [destOffset, offset, size, size] + codecopy ; [size] + ;; return memory content + push 0 ; [offset, size] + return ; [] +.end: diff --git a/cmd/hivechain/contracts/gencode.eas b/cmd/hivechain/contracts/gencode.eas new file mode 100644 index 0000000000..290355511d --- /dev/null +++ b/cmd/hivechain/contracts/gencode.eas @@ -0,0 +1,34 @@ +;;; -*- mode: asm -*- +;;; creates a contract with code based on block number. + +#include "rng.eas" + +#define outputSize 512 + +#define p_rng 0 +#define p_output p_rng + RngSize +#define p_outputEnd p_output + outputSize + +.start: + %RngInit(p_rng) ; [] + + ;; loop to create the code using rng + push p_output ; [ptr] +loop: + ;; store random word to output + %Rng(p_rng) ; [word, ptr] + dup2 ; [ptr, word, ptr] + mstore ; [ptr] + ;; increment output pointer + push 32 ; [1, ptr] + add ; [newptr] + push p_outputEnd ; [end, newptr] + dup2 ; [newptr, end, newtpr] + lt ; [newptr 0 { - prev := gen.PrevBlock(-1) - gasLimit = core.CalcGasLimit(prev.GasLimit(), cfg.genesis.GasLimit) - } - - var ( - txGasSum uint64 - txCount = 0 - accounts = make(map[common.Address]*ecdsa.PrivateKey) - ) - for addr, key := range knownAccounts { - if _, ok := cfg.genesis.Alloc[addr]; ok { - accounts[addr] = key - } - } + // for write/export + blockchain *core.BlockChain +} - for txCount <= cfg.txCount && len(accounts) > 0 { - for addr, key := range accounts { - tx := generateTx(txType, key, &cfg.genesis, gen) - // Check if account has enough balance left to cover the tx. - if gen.GetBalance(addr).Cmp(tx.Cost()) < 0 { - delete(accounts, addr) - continue - } - // Check if block gas limit reached. - if (txGasSum + tx.Gas()) > gasLimit { - return - } +type modifierInstance struct { + name string + blockModifier +} - log.Printf("adding tx (type %d) from %s in block %d", txType, addr.String(), gen.Number()) - log.Printf("%v (%d gas)", tx.Hash(), tx.Gas()) - gen.AddTx(tx) - txGasSum += tx.Gas() - txCount++ - } - } +type genAccount struct { + addr common.Address + key *ecdsa.PrivateKey } -// generateTx creates a random transaction signed by the given account. -func generateTx(txType int, key *ecdsa.PrivateKey, genesis *core.Genesis, gen *core.BlockGen) *types.Transaction { - var ( - src = crypto.PubkeyToAddress(key.PublicKey) - gasprice = big.NewInt(0) - tx *types.Transaction - ) - // Generate according to type. - switch txType { - case txTypeValue: - amount := big.NewInt(1) - var dst common.Address - rand.Read(dst[:]) - tx = types.NewTransaction(gen.TxNonce(src), dst, amount, params.TxGas, gasprice, nil) - case txTypeStorage: - gas := createTxGasLimit(gen, genesis, genstorage) + 80000 - tx = types.NewContractCreation(gen.TxNonce(src), new(big.Int), gas, gasprice, genstorage) - case txTypeLogs: - gas := createTxGasLimit(gen, genesis, genlogs) + 20000 - tx = types.NewContractCreation(gen.TxNonce(src), new(big.Int), gas, gasprice, genlogs) - case txTypeCode: - // The code generator contract deploys any data given after its own bytecode. - codesize := 128 - input := make([]byte, len(gencode)+codesize) - copy(input, gencode) - rand.Read(input[len(gencode):]) - extraGas := 10000 + params.CreateDataGas*uint64(codesize) - gas := createTxGasLimit(gen, genesis, gencode) + extraGas - tx = types.NewContractCreation(gen.TxNonce(src), new(big.Int), gas, gasprice, input) +func newGenerator(cfg generatorConfig) *generator { + genesis := cfg.createGenesis() + return &generator{ + cfg: cfg, + genesis: genesis, + td: new(big.Int).Set(genesis.Difficulty), + modlist: cfg.createBlockModifiers(), + accounts: slices.Clone(knownAccounts), } - // Sign the transaction. - signer := types.MakeSigner(genesis.Config, gen.Number(), gen.Timestamp()) - signedTx, err := types.SignTx(tx, signer, key) - if err != nil { - panic(err) - } - return signedTx } -func createTxGasLimit(gen *core.BlockGen, genesis *core.Genesis, data []byte) uint64 { - isHomestead := genesis.Config.IsHomestead(gen.Number()) - isEIP2028 := genesis.Config.IsIstanbul(gen.Number()) - isEIP3860 := genesis.Config.IsShanghai(gen.Number(), gen.Timestamp()) - igas, err := core.IntrinsicGas(data, nil, true, isHomestead, isEIP2028, isEIP3860) - if err != nil { - panic(err) - } - return igas +func (cfg *generatorConfig) createBlockModifiers() (list []*modifierInstance) { + for name, new := range modRegistry { + list = append(list, &modifierInstance{ + name: name, + blockModifier: new(), + }) + } + slices.SortFunc(list, func(a, b *modifierInstance) int { + return strings.Compare(a.name, b.name) + }) + return list } -// generateAndSave produces a chain based on the config. -func (cfg generatorConfig) generateAndSave(path string, blockModifier func(i int, gen *core.BlockGen)) error { +// run produces a chain. +func (g *generator) run() error { db := rawdb.NewMemoryDatabase() triedb := trie.NewDatabase(db, trie.HashDefaults) - genesis := cfg.genesis.MustCommit(db, triedb) - config := cfg.genesis.Config + genesis := g.genesis.MustCommit(db, triedb) + config := g.genesis.Config + powEngine := ethash.NewFaker() posEngine := beacon.New(powEngine) - engine := instaSeal{posEngine} + engine := posEngine // Create the PoW chain. - chain, _ := core.GenerateChain(config, genesis, engine, db, cfg.blockCount, blockModifier) - - // Create the PoS chain extension. - if cfg.isPoS { - // Set TTD to the head of the PoW chain. - totalDifficulty := big.NewInt(0) - for _, b := range chain { - totalDifficulty.Add(totalDifficulty, b.Difficulty()) - } - config.TerminalTotalDifficulty = totalDifficulty - - posChain, _ := core.GenerateChain(config, chain[len(chain)-1], engine, db, cfg.posBlockCount, blockModifier) - chain = append(chain, posChain...) - } + chain, _ := core.GenerateChain(config, genesis, engine, db, g.cfg.chainLength, g.modifyBlock) // Import the chain. This runs all block validation rules. - blockchain, err := core.NewBlockChain(db, nil, &cfg.genesis, nil, engine, vm.Config{}, nil, nil) + cacheconfig := core.DefaultCacheConfigWithScheme("hash") + cacheconfig.Preimages = true + vmconfig := vm.Config{EnablePreimageRecording: true} + blockchain, err := core.NewBlockChain(db, cacheconfig, g.genesis, nil, engine, vmconfig, nil, nil) if err != nil { return fmt.Errorf("can't create blockchain: %v", err) } defer blockchain.Stop() - // error out if blockchain config is nil -- avoid hanging chain generation - if blockchain.Config() == nil { - return fmt.Errorf("cannot insert chain with nil chain config") - } - if _, err := blockchain.InsertChain(chain); err != nil { - return fmt.Errorf("chain validation error: %v", err) + if i, err := blockchain.InsertChain(chain); err != nil { + return fmt.Errorf("chain validation error (block %d): %v", chain[i].Number(), err) } - headstate, _ := blockchain.State() - dump := headstate.Dump(&state.DumpConfig{}) - // Write out the generated blockchain - if err := writeChain(blockchain, filepath.Join(path, "chain.rlp"), 1, cfg.modifyBlock); err != nil { - return err - } - if err := writeChain(blockchain, filepath.Join(path, "chain_genesis.rlp"), 0, cfg.modifyBlock); err != nil { - return err - } - if err := os.WriteFile(filepath.Join(path, "chain_poststate.json"), dump, 0644); err != nil { - return err - } - return nil + // Write the outputs. + g.blockchain = blockchain + return g.write() } -// ethashDir returns the directory for storing ethash datasets. -func ethashDir() string { - home, err := os.UserHomeDir() - if err != nil { - return "" - } - return filepath.Join(home, ".ethash") +func (g *generator) modifyBlock(i int, gen *core.BlockGen) { + fmt.Println("generating block", gen.Number()) + g.setDifficulty(i, gen) + g.runModifiers(i, gen) } -// writeChain exports the given chain to a file. -func writeChain(chain *core.BlockChain, filename string, start uint64, modifyBlock func(*types.Block) *types.Block) error { - out, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - return err +func (g *generator) setDifficulty(i int, gen *core.BlockGen) { + chaincfg := g.genesis.Config + mergeblock := chaincfg.MergeNetsplitBlock + if mergeblock == nil { + mergeblock = new(big.Int).SetUint64(math.MaxUint64) } - defer out.Close() - lastBlock := chain.CurrentBlock().Number.Uint64() - return exportN(chain, out, start, lastBlock, modifyBlock) -} - -// instaSeal wraps a consensus engine with instant block sealing. When a block is produced -// using FinalizeAndAssemble, it also applies Seal. -type instaSeal struct{ consensus.Engine } - -// FinalizeAndAssemble implements consensus.Engine, accumulating the block and uncle rewards, -// setting the final state and assembling the block. -func (e instaSeal) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { - block, err := e.Engine.FinalizeAndAssemble(chain, header, state, txs, uncles, receipts, withdrawals) - if err != nil { - return nil, err + mergecmp := gen.Number().Cmp(mergeblock) + if mergecmp > 0 { + gen.SetPoS() + return } - sealedBlock := make(chan *types.Block, 1) - if err = e.Engine.Seal(nil, block, sealedBlock, nil); err != nil { - return nil, err + + prev := gen.PrevBlock(i - 1) + diff := ethash.CalcDifficulty(g.genesis.Config, gen.Timestamp(), prev.Header()) + if mergecmp == 0 { + gen.SetPoS() + chaincfg.TerminalTotalDifficulty = new(big.Int).Set(g.td) + } else { + g.td = g.td.Add(g.td, diff) + gen.SetDifficulty(diff) } - return <-sealedBlock, nil } -func exportN(bc *core.BlockChain, w io.Writer, first uint64, last uint64, modifyBlock func(*types.Block) *types.Block) error { - fmt.Printf("Exporting batch of blocks, count %v \n", last-first+1) +// runModifiers executes the chain modifiers. +func (g *generator) runModifiers(i int, gen *core.BlockGen) { + if len(g.modlist) == 0 || g.cfg.txInterval == 0 || i%g.cfg.txInterval != 0 { + return + } - start, reported := time.Now(), time.Now() - for nr := first; nr <= last; nr++ { - block := bc.GetBlockByNumber(nr) - if block == nil { - return fmt.Errorf("export failed on #%d: not found", nr) - } - block = modifyBlock(block) - if err := block.EncodeRLP(w); err != nil { - return err - } - if time.Since(reported) >= 8*time.Second { - fmt.Printf("Exporting blocks, exported: %v, elapsed: %v \n", block.NumberU64()-first, common.PrettyDuration(time.Since(start))) - reported = time.Now() + ctx := &genBlockContext{index: i, block: gen, gen: g} + if gen.Number().Uint64() > 0 { + prev := gen.PrevBlock(-1) + ctx.gasLimit = core.CalcGasLimit(prev.GasLimit(), g.genesis.GasLimit) + } + + // Modifier scheduling: we cycle through the available modifiers until enough have + // executed successfully. It also stops when all of them return false from apply() + // because this usually means there is no gas left. + count := 0 + refused := 0 // count of consecutive times apply() returned false + for ; count < g.cfg.txCount && refused < len(g.modlist); g.modOffset++ { + index := g.modOffset % len(g.modlist) + mod := g.modlist[index] + ok := mod.apply(ctx) + if ok { + fmt.Println(" -", mod.name) + count++ + refused = 0 + } else { + refused++ } } - return nil } diff --git a/cmd/hivechain/genesis.go b/cmd/hivechain/genesis.go new file mode 100644 index 0000000000..31750f6a44 --- /dev/null +++ b/cmd/hivechain/genesis.go @@ -0,0 +1,174 @@ +package main + +import ( + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" + "golang.org/x/exp/slices" +) + +var initialBalance, _ = new(big.Int).SetString("1000000000000000000000000000000000000", 10) + +const ( + ethashMinimumDifficulty = 131072 + genesisBaseFee = params.InitialBaseFee + blocktimeSec = 10 // hard-coded in core.GenerateChain +) + +// Ethereum mainnet forks in order of introduction. +var ( + allForkNames = append(numberBasedForkNames, timeBasedForkNames...) + lastFork = allForkNames[len(allForkNames)-1] + + numberBasedForkNames = []string{ + "homestead", + "eip150", + "eip155", + "eip158", + "byzantium", + "constantinople", + "petersburg", + "istanbul", + "muirglacier", + "berlin", + "london", + "arrowglacier", + "grayglacier", + "merge", + } + + timeBasedForkNames = []string{ + "shanghai", + "cancun", + "prague", + } +) + +// createChainConfig creates a chain configuration. +func (cfg *generatorConfig) createChainConfig() *params.ChainConfig { + chaincfg := new(params.ChainConfig) + + chainid, _ := new(big.Int).SetString("35039958740849263516136087381459012528369084397061947147216452157383146382873", 10) + chaincfg.ChainID = chainid + chaincfg.Ethash = new(params.EthashConfig) + + // Apply forks. + forks := cfg.forkBlocks() + for fork, b := range forks { + timestamp := cfg.blockTimestamp(b) + + switch fork { + // number-based forks + case "homestead": + chaincfg.HomesteadBlock = new(big.Int).SetUint64(b) + case "eip150": + chaincfg.EIP150Block = new(big.Int).SetUint64(b) + case "eip155": + chaincfg.EIP155Block = new(big.Int).SetUint64(b) + case "eip158": + chaincfg.EIP158Block = new(big.Int).SetUint64(b) + case "byzantium": + chaincfg.ByzantiumBlock = new(big.Int).SetUint64(b) + case "constantinople": + chaincfg.ConstantinopleBlock = new(big.Int).SetUint64(b) + case "petersburg": + chaincfg.PetersburgBlock = new(big.Int).SetUint64(b) + case "istanbul": + chaincfg.IstanbulBlock = new(big.Int).SetUint64(b) + case "muirglacier": + chaincfg.MuirGlacierBlock = new(big.Int).SetUint64(b) + case "berlin": + chaincfg.BerlinBlock = new(big.Int).SetUint64(b) + case "london": + chaincfg.LondonBlock = new(big.Int).SetUint64(b) + case "arrowglacier": + chaincfg.ArrowGlacierBlock = new(big.Int).SetUint64(b) + case "grayglacier": + chaincfg.GrayGlacierBlock = new(big.Int).SetUint64(b) + case "merge": + chaincfg.MergeNetsplitBlock = new(big.Int).SetUint64(b) + // time-based forks + case "shanghai": + chaincfg.ShanghaiTime = ×tamp + case "cancun": + chaincfg.CancunTime = ×tamp + case "prague": + chaincfg.PragueTime = ×tamp + } + } + + // Special case for merged-from-genesis networks. + // Need to assign TTD here because the genesis block won't be processed by GenerateChain. + if chaincfg.MergeNetsplitBlock != nil && chaincfg.MergeNetsplitBlock.Sign() == 0 { + chaincfg.TerminalTotalDifficulty = big.NewInt(ethashMinimumDifficulty) + } + + return chaincfg +} + +// createGenesis creates the genesis block and config. +func (cfg *generatorConfig) createGenesis() *core.Genesis { + var g core.Genesis + g.Config = cfg.createChainConfig() + + // Block attributes. + g.Difficulty = big.NewInt(ethashMinimumDifficulty) + g.ExtraData = []byte("hivechain") + g.GasLimit = params.GenesisGasLimit * 8 + zero := new(big.Int) + if g.Config.IsLondon(zero) { + g.BaseFee = big.NewInt(genesisBaseFee) + } + + // Initialize allocation. + // Here we add balance to the known accounts. + g.Alloc = make(core.GenesisAlloc) + for _, acc := range knownAccounts { + g.Alloc[acc.addr] = core.GenesisAccount{Balance: initialBalance} + } + + // Also deploy the beacon chain deposit contract. + // dca := common.HexToAddress(depositContractAddr) + // dcc := hexutil.MustDecode("0x" + depositCode) + // g.Alloc[dca] = core.GenesisAccount{Code: dcc} + + return &g +} + +// forkBlocks computes the block numbers where forks occur. Forks get enabled based on the +// forkInterval. If the total number of requested blocks (chainLength) is lower than +// necessary, the remaining forks activate on the last chain block. +func (cfg *generatorConfig) forkBlocks() map[string]uint64 { + lastIndex := cfg.lastForkIndex() + forks := allForkNames[:lastIndex+1] + forkBlocks := make(map[string]uint64) + for block := 0; block <= cfg.chainLength && len(forks) > 0; { + fork := forks[0] + forks = forks[1:] + forkBlocks[fork] = uint64(block) + block += cfg.forkInterval + } + for _, f := range forks { + forkBlocks[f] = uint64(cfg.chainLength) + } + return forkBlocks +} + +// lastForkIndex returns the index of the latest enabled for in allForkNames. +func (cfg *generatorConfig) lastForkIndex() int { + if cfg.lastFork == "" { + return len(allForkNames) - 1 + } + index := slices.Index(allForkNames, strings.ToLower(cfg.lastFork)) + if index == -1 { + panic(fmt.Sprintf("unknown lastFork name %q", cfg.lastFork)) + } + return index +} + +func (cfg *generatorConfig) blockTimestamp(num uint64) uint64 { + return num * blocktimeSec +} diff --git a/cmd/hivechain/genlogs.evm b/cmd/hivechain/genlogs.evm deleted file mode 100644 index 0641c6a0ac..0000000000 --- a/cmd/hivechain/genlogs.evm +++ /dev/null @@ -1,42 +0,0 @@ -;;; -*- mode: asm -*- -;;; creates random logs until less than 10k gas remaining. - - ;; initialize random generator: - ;; block number at offset 0, counter at 32 - number - push 0 - mstore - push 0 - push 32 - mstore - -genlogs: - push @genlogs_emit - jump @random -genlogs_emit: - push 32 - push 32 - log1 - ;; enough gas left? loop! - gas - push 10000 - lt - jumpi @genlogs - stop - -;;; ( rdest -- x ) creates random numbers based on block number -random: - ;; increment - push 32 - mload - push 1 - add - push 32 - mstore - ;; compute - push 64 - push 0 - sha3 - ;; return to caller - swap1 - jump diff --git a/cmd/hivechain/genstorage.evm b/cmd/hivechain/genstorage.evm deleted file mode 100644 index 77d0290db4..0000000000 --- a/cmd/hivechain/genstorage.evm +++ /dev/null @@ -1,16 +0,0 @@ -;;; -*- mode: asm -*- -;;; creates storage slots until less than 25k gas remaining. - - number -genstorage: - dup1 - dup1 - sstore - push 1 - add - ;; enough gas left? loop! - gas - push 25000 - lt - jumpi @genstorage - stop diff --git a/cmd/hivechain/main.go b/cmd/hivechain/main.go index e1c109f622..2d9d373b73 100644 --- a/cmd/hivechain/main.go +++ b/cmd/hivechain/main.go @@ -24,34 +24,75 @@ import ( "fmt" "io" "os" + "strings" "github.com/ethereum/go-ethereum/core/types" ethlog "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" ) -const usage = "Usage: hivechain generate|print|print-genesis|trim [ options ] ..." - func main() { // Initialize go-ethereum logging. // This is mostly for displaying the DAG generator progress. handler := ethlog.StreamHandler(os.Stderr, ethlog.TerminalFormat(false)) - ethlog.Root().SetHandler(ethlog.LvlFilterHandler(ethlog.LvlInfo, handler)) + ethlog.Root().SetHandler(ethlog.LvlFilterHandler(ethlog.LvlWarn, handler)) + + flag.Usage = usage if len(os.Args) < 2 { - fatalf(usage) + flag.Usage() + os.Exit(1) } switch os.Args[1] { case "generate": generateCommand(os.Args[2:]) case "print": printCommand(os.Args[2:]) - case "print-genesis": - printGenesisCommand(os.Args[2:]) - case "trim": - trimCommand(os.Args[2:]) default: - fatalf(usage) + flag.Usage() + os.Exit(1) + } +} + +// generateCommand generates a test chain. +func generateCommand(args []string) { + var ( + cfg generatorConfig + outlist = flag.String("outputs", "", "Enabled output modules") + ) + flag.IntVar(&cfg.chainLength, "length", 2, "The length of the pow chain to generate") + flag.IntVar(&cfg.txInterval, "tx-interval", 10, "Add transactions to chain every n blocks") + flag.IntVar(&cfg.txCount, "tx-count", 1, "Maximum number of txs per block") + flag.IntVar(&cfg.forkInterval, "fork-interval", 0, "Number of blocks between fork activations") + flag.StringVar(&cfg.outputDir, "outdir", ".", "Destination directory") + flag.StringVar(&cfg.lastFork, "lastfork", "", "Name of the last fork to activate") + flag.CommandLine.Parse(args) + + if *outlist != "" { + if *outlist == "all" { + cfg.outputs = outputFunctionNames() + } + cfg.outputs = splitAndTrim(*outlist) + } + + cfg, err := cfg.withDefaults() + if err != nil { + panic(err) + } + g := newGenerator(cfg) + if err := g.run(); err != nil { + fatal(err) + } +} + +func usage() { + o := flag.CommandLine.Output() + fmt.Fprintln(o, "Usage: hivechain generate|print [options...]") + flag.PrintDefaults() + fmt.Fprintln(o, "") + fmt.Fprintln(o, "List of available -outputs:") + for _, name := range outputFunctionNames() { + fmt.Fprintln(o, " ", name) } } @@ -89,97 +130,15 @@ func printCommand(args []string) { } } -// printGenesisCommand displays the genesis post-state. -func printGenesisCommand(args []string) { - flag.CommandLine.Parse(args) - if flag.NArg() != 1 { - fatalf("Usage: hivechain print-genesis ") - } - - gspec, err := loadGenesis(flag.Arg(0)) - if err != nil { - fatal(err) - } - block := gspec.ToBlock() - js, _ := json.MarshalIndent(block.Header(), "", " ") - fmt.Println(string(js)) -} - -// trimCommand exports a subset of chain.rlp to a new file. -func trimCommand(args []string) { - var ( - from = flag.Uint("from", 0, "Start of block range to output") - to = flag.Uint("to", 0, "End of block range to output (0 = all blocks)") - ) - flag.CommandLine.Parse(args) - if flag.NArg() != 2 { - fatalf("Usage: hivechain trim [ options ] ") - } - if *to > 0 && *to <= *from { - fatalf("-to must be greater than -from") - } - - input, err := os.Open(flag.Arg(0)) - if err != nil { - fatal(err) - } - defer input.Close() - - output, err := os.OpenFile(flag.Arg(1), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - fatal(err) - } - defer output.Close() - - s := rlp.NewStream(bufio.NewReader(input), 0) - written := 0 - for i := uint(0); ; i++ { - data, err := s.Raw() - if err == io.EOF { - break - } else if err != nil { - fatalf("block %d: %v", i, err) - } - if i >= *from { - if *to != 0 && i >= *to { - break - } - output.Write(data) - written++ +func splitAndTrim(s string) []string { + var list []string + for _, s := range strings.Split(s, ",") { + s = strings.TrimSpace(s) + if s != "" { + list = append(list, s) } } - fmt.Println(written, "blocks written to", flag.Arg(1)) -} - -// generateCommand generates a test chain. -func generateCommand(args []string) { - var ( - cfg generatorConfig - genesis = flag.String("genesis", "", "The path and filename to the source genesis.json") - outdir = flag.String("output", ".", "Chain destination folder") - pos = flag.Bool("pos", false, "Enables PoS chain") - ) - flag.IntVar(&cfg.blockCount, "length", 2, "The length of the pow chain to generate") - flag.IntVar(&cfg.posBlockCount, "poslength", 2, "The length of the pos chain to generate") - flag.IntVar(&cfg.blockTimeSec, "blocktime", 30, "The desired block time in seconds") - flag.IntVar(&cfg.txInterval, "tx-interval", 10, "Add transactions to chain every n blocks") - flag.IntVar(&cfg.txCount, "tx-count", 1, "Maximum number of txs per block") - flag.CommandLine.Parse(args) - - if *genesis == "" { - fatalf("Missing -genesis option, please supply a genesis.json file.") - } - cfg.isPoS = *pos - - gspec, err := loadGenesis(*genesis) - if err != nil { - fatal(err) - } - cfg.genesis = *gspec - - if err := cfg.writeTestChain(*outdir); err != nil { - fatal(err) - } + return list } func fatalf(format string, args ...interface{}) { diff --git a/cmd/hivechain/mod.go b/cmd/hivechain/mod.go new file mode 100644 index 0000000000..cfc8b7d16e --- /dev/null +++ b/cmd/hivechain/mod.go @@ -0,0 +1,113 @@ +package main + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +type blockModifier interface { + apply(*genBlockContext) bool + txInfo() any +} + +var modRegistry = make(map[string]func() blockModifier) + +// register adds a block modifier. +func register(name string, new func() blockModifier) { + modRegistry[name] = new +} + +type genBlockContext struct { + index int + block *core.BlockGen + gen *generator + + gasLimit uint64 + txCount int +} + +// Number returns the block number. +func (ctx *genBlockContext) Number() *big.Int { + return ctx.block.Number() +} + +// NumberU64 returns the block number. +func (ctx *genBlockContext) NumberU64() uint64 { + return ctx.block.Number().Uint64() +} + +// Timestamp returns the block timestamp. +func (ctx *genBlockContext) Timestamp() uint64 { + return ctx.block.Timestamp() +} + +// HasGas reports whether the block still has more than the given amount of gas left. +func (ctx *genBlockContext) HasGas(gas uint64) bool { + return ctx.gasLimit > gas +} + +// AddNewTx adds a transaction into the block. +func (ctx *genBlockContext) AddNewTx(sender *genAccount, data types.TxData) *types.Transaction { + tx, err := types.SignNewTx(sender.key, ctx.Signer(), data) + if err != nil { + panic(err) + } + if ctx.gasLimit < tx.Gas() { + panic("not enough gas for tx") + } + ctx.block.AddTx(tx) + ctx.gasLimit -= tx.Gas() + return tx +} + +// TxSenderAccount chooses an account to send transactions from. +func (ctx *genBlockContext) TxSenderAccount() *genAccount { + a := ctx.gen.accounts[0] + return &a +} + +// TxCreateIntrinsicGas gives the 'intrinsic gas' of a contract creation transaction. +func (ctx *genBlockContext) TxCreateIntrinsicGas(data []byte) uint64 { + genesis := ctx.gen.genesis + isHomestead := genesis.Config.IsHomestead(ctx.block.Number()) + isEIP2028 := genesis.Config.IsIstanbul(ctx.block.Number()) + isEIP3860 := genesis.Config.IsShanghai(ctx.block.Number(), ctx.block.Timestamp()) + igas, err := core.IntrinsicGas(data, nil, true, isHomestead, isEIP2028, isEIP3860) + if err != nil { + panic(err) + } + return igas +} + +// TxGasFeeCap returns the minimum gasprice that should be used for transactions. +func (ctx *genBlockContext) TxGasFeeCap() *big.Int { + fee := big.NewInt(1) + if !ctx.ChainConfig().IsLondon(ctx.block.Number()) { + return fee + } + return fee.Add(fee, ctx.block.BaseFee()) +} + +// AccountNonce returns the current nonce of an address. +func (ctx *genBlockContext) AccountNonce(addr common.Address) uint64 { + return ctx.block.TxNonce(addr) +} + +// Signer returns a signer for the current block. +func (ctx *genBlockContext) Signer() types.Signer { + return types.MakeSigner(ctx.ChainConfig(), ctx.block.Number(), ctx.block.Timestamp()) +} + +// ChainConfig returns the chain config. +func (ctx *genBlockContext) ChainConfig() *params.ChainConfig { + return ctx.gen.genesis.Config +} + +// ParentBlock returns the parent of the current block. +func (ctx *genBlockContext) ParentBlock() *types.Block { + return ctx.block.PrevBlock(ctx.index - 1) +} diff --git a/cmd/hivechain/mod_deploy.go b/cmd/hivechain/mod_deploy.go new file mode 100644 index 0000000000..b11b84951a --- /dev/null +++ b/cmd/hivechain/mod_deploy.go @@ -0,0 +1,59 @@ +package main + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +func init() { + register("deploy-callme", func() blockModifier { + return &modDeploy{code: callmeCode} + }) + register("deploy-callenv", func() blockModifier { + return &modDeploy{code: callenvCode} + }) +} + +type modDeploy struct { + code []byte + info *deployTxInfo +} + +type deployTxInfo struct { + Contract common.Address `json:"contract"` + Block hexutil.Uint64 `json:"block"` +} + +func (m *modDeploy) apply(ctx *genBlockContext) bool { + if m.info != nil { + return false // already deployed + } + + var code []byte + code = append(code, deployerCode...) + code = append(code, m.code...) + gas := ctx.TxCreateIntrinsicGas(code) + 10000 + if !ctx.HasGas(gas) { + return false + } + + sender := ctx.TxSenderAccount() + nonce := ctx.AccountNonce(sender.addr) + ctx.AddNewTx(sender, &types.LegacyTx{ + Nonce: nonce, + Gas: gas, + GasPrice: ctx.TxGasFeeCap(), + Data: code, + }) + m.info = &deployTxInfo{ + Contract: crypto.CreateAddress(sender.addr, nonce), + Block: hexutil.Uint64(ctx.block.Number().Uint64()), + } + return true +} + +func (m *modDeploy) txInfo() any { + return m.info +} diff --git a/cmd/hivechain/mod_randtx.go b/cmd/hivechain/mod_randtx.go new file mode 100644 index 0000000000..3e91942aff --- /dev/null +++ b/cmd/hivechain/mod_randtx.go @@ -0,0 +1,52 @@ +package main + +import ( + "github.com/ethereum/go-ethereum/core/types" +) + +func init() { + register("randomlogs", func() blockModifier { + return &modCreateTx{ + code: genlogsCode, + gas: 20000, + } + }) + register("randomcode", func() blockModifier { + return &modCreateTx{ + code: gencodeCode, + gas: 30000, + } + }) + register("randomstorage", func() blockModifier { + return &modCreateTx{ + code: genstorageCode, + gas: 80000, + } + }) +} + +type modCreateTx struct { + code []byte + gas uint64 +} + +func (m *modCreateTx) apply(ctx *genBlockContext) bool { + gas := ctx.TxCreateIntrinsicGas(m.code) + m.gas + if !ctx.HasGas(gas) { + return false + } + + sender := ctx.TxSenderAccount() + txdata := &types.LegacyTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: gas, + GasPrice: ctx.TxGasFeeCap(), + Data: m.code, + } + ctx.AddNewTx(sender, txdata) + return true +} + +func (m *modCreateTx) txInfo() any { + return nil +} diff --git a/cmd/hivechain/mod_uncles.go b/cmd/hivechain/mod_uncles.go new file mode 100644 index 0000000000..20c23afba2 --- /dev/null +++ b/cmd/hivechain/mod_uncles.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" +) + +func init() { + register("uncles", func() blockModifier { + return &modUncles{ + info: make(map[uint64]unclesInfo), + } + }) +} + +type modUncles struct { + info map[uint64]unclesInfo + counter int +} + +type unclesInfo struct { + Hashes []common.Hash `json:"hashes"` +} + +func (m *modUncles) apply(ctx *genBlockContext) bool { + merge := ctx.ChainConfig().MergeNetsplitBlock + if merge != nil && merge.Cmp(ctx.Number()) <= 0 { + return false // no uncles after merge + } + if ctx.NumberU64() < 3 { + return false + } + info := m.info[ctx.NumberU64()] + if len(info.Hashes) >= 2 { + return false + } + + parent := ctx.ParentBlock() + uncle := &types.Header{ + Number: parent.Number(), + ParentHash: parent.ParentHash(), + Extra: []byte(fmt.Sprintf("hivechain uncle %d", m.counter)), + } + ub := types.NewBlock(uncle, nil, nil, nil, trie.NewStackTrie(nil)) + uncle = ub.Header() + ctx.block.AddUncle(uncle) + info.Hashes = append(info.Hashes, uncle.Hash()) + m.info[ctx.NumberU64()] = info + m.counter++ + return true +} + +func (m *modUncles) txInfo() any { + return m.info +} diff --git a/cmd/hivechain/mod_valuetransfer.go b/cmd/hivechain/mod_valuetransfer.go new file mode 100644 index 0000000000..0597ecbb49 --- /dev/null +++ b/cmd/hivechain/mod_valuetransfer.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto/sha256" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func init() { + register("valuetransfer", func() blockModifier { + return &modValueTransfer{} + }) +} + +type modValueTransfer struct { + counter int + txs []valueTransferInfo +} + +type valueTransferInfo struct { + Block hexutil.Uint64 `json:"block"` + Sender common.Address `json:"sender"` + Tx *types.Transaction `json:"tx"` +} + +func (m *modValueTransfer) apply(ctx *genBlockContext) bool { + if !ctx.HasGas(params.TxGas) { + return false + } + + sender := ctx.TxSenderAccount() + var txdata types.TxData + for txdata == nil { + switch m.counter % 2 { + case 0: + // legacy tx + r := randomRecipient(ctx.Number()) + txdata = &types.LegacyTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: params.TxGas, + GasPrice: ctx.TxGasFeeCap(), + To: &r, + Value: big.NewInt(1), + } + case 1: + // EIP1559 tx + if !ctx.ChainConfig().IsLondon(ctx.Number()) { + m.counter++ + continue + } + r := randomRecipient(ctx.Number()) + txdata = &types.DynamicFeeTx{ + Nonce: ctx.AccountNonce(sender.addr), + Gas: params.TxGas, + GasFeeCap: ctx.TxGasFeeCap(), + GasTipCap: big.NewInt(1), + To: &r, + Value: big.NewInt(1), + } + } + } + + tx := ctx.AddNewTx(sender, txdata) + m.counter++ + m.txs = append(m.txs, valueTransferInfo{ + Block: hexutil.Uint64(ctx.NumberU64()), + Sender: sender.addr, + Tx: tx, + }) + return true +} + +func (m *modValueTransfer) txInfo() any { + return m.txs +} + +func randomRecipient(blocknum *big.Int) (a common.Address) { + h := sha256.Sum256(blocknum.Bytes()) + a.SetBytes(h[:20]) + return a +} diff --git a/cmd/hivechain/mod_withdrawals.go b/cmd/hivechain/mod_withdrawals.go new file mode 100644 index 0000000000..1154543020 --- /dev/null +++ b/cmd/hivechain/mod_withdrawals.go @@ -0,0 +1,45 @@ +package main + +import ( + "github.com/ethereum/go-ethereum/core/types" +) + +func init() { + register("withdrawals", func() blockModifier { + return &modWithdrawals{ + info: make(map[uint64]withdrawalsInfo), + } + }) +} + +type modWithdrawals struct { + info map[uint64]withdrawalsInfo +} + +type withdrawalsInfo struct { + Withdrawals []*types.Withdrawal `json:"withdrawals"` +} + +func (m *modWithdrawals) apply(ctx *genBlockContext) bool { + if !ctx.ChainConfig().IsShanghai(ctx.Number(), ctx.Timestamp()) { + return false + } + info := m.info[ctx.NumberU64()] + if len(info.Withdrawals) >= 2 { + return false + } + + w := types.Withdrawal{ + Validator: 5, + Address: randomRecipient(ctx.Number()), + Amount: 100, + } + w.Index = ctx.block.AddWithdrawal(&w) + info.Withdrawals = append(info.Withdrawals, &w) + m.info[ctx.NumberU64()] = info + return true +} + +func (m *modWithdrawals) txInfo() any { + return m.info +} diff --git a/cmd/hivechain/output.go b/cmd/hivechain/output.go new file mode 100644 index 0000000000..931f35b7bb --- /dev/null +++ b/cmd/hivechain/output.go @@ -0,0 +1,164 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "golang.org/x/exp/maps" +) + +var outputFunctions = map[string]func(*generator) error{ + "genesis": (*generator).writeGenesis, + "chain": (*generator).writeChain, + "powchain": (*generator).writePoWChain, + "headstate": (*generator).writeState, + "headblock": (*generator).writeHeadBlock, + "accounts": (*generator).writeAccounts, + "txinfo": (*generator).writeTxInfo, + "headfcu": (*generator).writeEngineHeadFcU, + "fcu": (*generator).writeEngineFcU, + "newpayload": (*generator).writeEngineNewPayload, +} + +func outputFunctionNames() []string { + names := maps.Keys(outputFunctions) + sort.Strings(names) + return names +} + +// write creates the generator output files. +func (g *generator) write() error { + var wf []func(*generator) error + for _, name := range g.cfg.outputs { + fmt.Println("writing", name) + f := outputFunctions[name] + if f == nil { + return fmt.Errorf("unknown output %q", name) + } + wf = append(wf, f) + } + for _, f := range wf { + if err := f(g); err != nil { + return err + } + } + return nil +} + +func (g *generator) openOutputFile(file string) (*os.File, error) { + path := filepath.Join(g.cfg.outputDir, file) + return os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) +} + +func (g *generator) writeJSON(name string, obj any) error { + jsonData, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + out, err := g.openOutputFile(name) + if err != nil { + return err + } + defer out.Close() + _, err = out.Write(jsonData) + return err +} + +// writeGenesis writes the genesis.json file. +func (g *generator) writeGenesis() error { + return g.writeJSON("genesis.json", g.genesis) +} + +// writeAccounts writes the account keys file. +func (g *generator) writeAccounts() error { + type accountObj struct { + Key string `json:"key"` + } + m := make(map[common.Address]*accountObj, len(g.accounts)) + for _, a := range g.accounts { + m[a.addr] = &accountObj{ + Key: hexutil.Encode(a.key.D.Bytes()), + } + } + return g.writeJSON("accounts.json", &m) +} + +// writeState writes the chain state dump. +func (g *generator) writeState() error { + headstate, err := g.blockchain.State() + if err != nil { + return err + } + dump := headstate.RawDump(&state.DumpConfig{}) + return g.writeJSON("headstate.json", &dump) +} + +// writeHeadBlock writes information about the head block. +func (g *generator) writeHeadBlock() error { + return g.writeJSON("headblock.json", g.blockchain.CurrentHeader()) +} + +// writeChain writes all RLP blocks to a file. +func (g *generator) writeChain() error { + path := filepath.Join(g.cfg.outputDir, "chain.rlp") + out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer out.Close() + lastBlock := g.blockchain.CurrentBlock().Number.Uint64() + return exportN(g.blockchain, out, 0, lastBlock) +} + +// writePoWChain writes pre-merge RLP blocks to a file. +func (g *generator) writePoWChain() error { + path := filepath.Join(g.cfg.outputDir, "powchain.rlp") + out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer out.Close() + lastBlock, ok := g.mergeBlock() + if !ok { + lastBlock = g.blockchain.CurrentBlock().Number.Uint64() + } + return exportN(g.blockchain, out, 0, lastBlock) +} + +func (g *generator) mergeBlock() (uint64, bool) { + merge := g.genesis.Config.MergeNetsplitBlock + if merge != nil { + return merge.Uint64(), true + } + return 0, false +} + +func exportN(bc *core.BlockChain, w io.Writer, first uint64, last uint64) error { + for nr := first; nr <= last; nr++ { + block := bc.GetBlockByNumber(nr) + if block == nil { + return fmt.Errorf("export failed on #%d: not found", nr) + } + if err := block.EncodeRLP(w); err != nil { + return err + } + } + return nil +} + +// writeTxInfo writes information about the transactions that were added into the chain. +func (g *generator) writeTxInfo() error { + m := make(map[string]any, len(g.modlist)) + for _, inst := range g.modlist { + m[inst.name] = inst.txInfo() + } + return g.writeJSON("txinfo.json", &m) +} diff --git a/cmd/hivechain/output_engine.go b/cmd/hivechain/output_engine.go new file mode 100644 index 0000000000..ec124fdc49 --- /dev/null +++ b/cmd/hivechain/output_engine.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// writeEngineNewPayload writes engine API newPayload requests for the chain. +// Note this only works for post-merge blocks. +func (g *generator) writeEngineNewPayload() error { + list := make([]*rpcRequest, 0) + start, ok := g.mergeBlock() + if ok { + last := g.blockchain.CurrentBlock().Number.Uint64() + for num := start; num <= last; num++ { + b := g.blockchain.GetBlockByNumber(num) + list = append(list, g.block2newpayload(b)) + } + } + return g.writeJSON("newpayload.json", list) +} + +// writeEngineFcU writes engine API forkchoiceUpdated requests for the chain. +// Note this only works for post-merge blocks. +func (g *generator) writeEngineFcU() error { + list := make([]*rpcRequest, 0) + start, ok := g.mergeBlock() + if ok { + last := g.blockchain.CurrentBlock().Number.Uint64() + for num := start; num <= last; num++ { + b := g.blockchain.GetBlockByNumber(num) + list = append(list, g.block2fcu(b)) + } + } + return g.writeJSON("fcu.json", list) +} + +// writeEngineHeadFcU writes an engine API forkchoiceUpdated request for the head block. +func (g *generator) writeEngineHeadFcU() error { + h := g.blockchain.CurrentBlock() + b := g.blockchain.GetBlock(h.Hash(), h.Number.Uint64()) + fcu := g.block2fcu(b) + return g.writeJSON("headfcu.json", fcu) +} + +func (g *generator) block2newpayload(b *types.Block) *rpcRequest { + ed := engine.ExecutableData{ + ParentHash: b.ParentHash(), + FeeRecipient: b.Coinbase(), + StateRoot: b.Root(), + ReceiptsRoot: b.ReceiptHash(), + LogsBloom: b.Bloom().Bytes(), + Random: b.MixDigest(), + Number: b.NumberU64(), + GasLimit: b.GasLimit(), + GasUsed: b.GasUsed(), + Timestamp: b.Time(), + ExtraData: b.Extra(), + BaseFeePerGas: b.BaseFee(), + BlockHash: b.Hash(), + Transactions: [][]byte{}, + Withdrawals: b.Withdrawals(), + BlobGasUsed: b.BlobGasUsed(), + ExcessBlobGas: b.ExcessBlobGas(), + } + var blobHashes = make([]common.Hash, 0) + for _, tx := range b.Transactions() { + // Fill in transactions list. + bin, err := tx.MarshalBinary() + if err != nil { + panic(err) + } + ed.Transactions = append(ed.Transactions, bin) + + // Collect blob hashes for post-Cancun blocks. + for _, bh := range tx.BlobHashes() { + blobHashes = append(blobHashes, bh) + } + } + + var method string + var params = []any{ed} + cfg := g.genesis.Config + switch { + case cfg.IsCancun(b.Number(), b.Time()): + method = "engine_newPayloadV3" + params = append(params, blobHashes, b.BeaconRoot()) + case cfg.IsShanghai(b.Number(), b.Time()): + method = "engine_newPayloadV2" + default: + method = "engine_newPayloadV1" + } + id := fmt.Sprintf("np%d", b.NumberU64()) + return &rpcRequest{JsonRPC: "2.0", ID: id, Method: method, Params: params} +} + +func (g *generator) block2fcu(b *types.Block) *rpcRequest { + fc := engine.ForkchoiceStateV1{ + HeadBlockHash: b.Hash(), + SafeBlockHash: b.Hash(), + FinalizedBlockHash: b.Hash(), + } + var method string + cfg := g.genesis.Config + switch { + case cfg.IsCancun(b.Number(), b.Time()): + method = "engine_forkchoiceUpdatedV3" + case cfg.IsShanghai(b.Number(), b.Time()): + method = "engine_forkchoiceUpdatedV2" + default: + method = "engine_forkchoiceUpdatedV1" + } + id := fmt.Sprintf("fcu%d", b.NumberU64()) + return &rpcRequest{JsonRPC: "2.0", ID: id, Method: method, Params: []any{&fc, nil}} +} + +type rpcRequest struct { + JsonRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params []any `json:"params"` +}