From d94b880cb7850fad0b170983933718b625432eb6 Mon Sep 17 00:00:00 2001 From: Roy Crihfield Date: Mon, 8 Apr 2024 22:20:10 +0800 Subject: [PATCH] add chaingen & indexing test utils --- test_helpers/chaingen/contract.go | 30 ++++++ test_helpers/chaingen/gen.go | 150 ++++++++++++++++++++++++++++++ test_helpers/db.go | 33 +++++++ test_helpers/indexing.go | 100 ++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 test_helpers/chaingen/contract.go create mode 100644 test_helpers/chaingen/gen.go create mode 100644 test_helpers/db.go create mode 100644 test_helpers/indexing.go diff --git a/test_helpers/chaingen/contract.go b/test_helpers/chaingen/contract.go new file mode 100644 index 0000000..f4759e5 --- /dev/null +++ b/test_helpers/chaingen/contract.go @@ -0,0 +1,30 @@ +package chaingen + +import ( + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type ContractSpec struct { + DeploymentCode []byte + ABI abi.ABI +} + +func ParseContract(abiStr, binStr string) (*ContractSpec, error) { + parsedABI, err := abi.JSON(strings.NewReader(abiStr)) + if err != nil { + return nil, err + } + data := common.Hex2Bytes(binStr) + return &ContractSpec{data, parsedABI}, nil +} + +func MustParseContract(abiStr, binStr string) *ContractSpec { + spec, err := ParseContract(abiStr, binStr) + if err != nil { + panic(err) + } + return spec +} diff --git a/test_helpers/chaingen/gen.go b/test_helpers/chaingen/gen.go new file mode 100644 index 0000000..01bc90f --- /dev/null +++ b/test_helpers/chaingen/gen.go @@ -0,0 +1,150 @@ +package chaingen + +import ( + "crypto/ecdsa" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" +) + +const secondsPerBlock = 12 + +type GenContext struct { + ChainConfig *params.ChainConfig + GenFuncs []func(int, *core.BlockGen) + DB ethdb.Database + + Keys map[common.Address]*ecdsa.PrivateKey + Contracts map[string]*ContractSpec + Genesis *types.Block + + block *core.BlockGen // cache the current block for my methods' use + deployed map[common.Address]string // names of deployed contracts keyed by deployer + time uint64 // time at current block, in seconds +} + +func NewGenContext(chainConfig *params.ChainConfig, db ethdb.Database) *GenContext { + return &GenContext{ + ChainConfig: chainConfig, + DB: db, + Keys: make(map[common.Address]*ecdsa.PrivateKey), + Contracts: make(map[string]*ContractSpec), + + deployed: make(map[common.Address]string), + } +} + +func (gen *GenContext) AddFunction(fn func(int, *core.BlockGen)) { + gen.GenFuncs = append(gen.GenFuncs, fn) +} + +func (gen *GenContext) AddOwnedAccount(key *ecdsa.PrivateKey) common.Address { + addr := crypto.PubkeyToAddress(key.PublicKey) + gen.Keys[addr] = key + return addr +} + +func (gen *GenContext) AddContract(name string, spec *ContractSpec) { + gen.Contracts[name] = spec +} + +func (gen *GenContext) generate(i int, block *core.BlockGen) { + gen.block = block + for _, fn := range gen.GenFuncs { + fn(i, block) + } + gen.time += secondsPerBlock +} + +// MakeChain creates a chain of n blocks starting at and including the genesis block. +// the returned hash chain is ordered head->parent. +func (gen *GenContext) MakeChain(n int) ([]*types.Block, []types.Receipts, *core.BlockChain) { + blocks, receipts := core.GenerateChain( + gen.ChainConfig, gen.Genesis, ethash.NewFaker(), gen.DB, n, gen.generate, + ) + chain, err := core.NewBlockChain(gen.DB, nil, nil, nil, ethash.NewFaker(), vm.Config{}, nil, nil) + if err != nil { + panic(err) + } + return append([]*types.Block{gen.Genesis}, blocks...), receipts, chain +} + +func (gen *GenContext) CreateSendTx(from common.Address, to common.Address, amount *big.Int) (*types.Transaction, error) { + return gen.createTx(from, &to, amount, params.TxGas, nil) +} + +func (gen *GenContext) CreateContractTx(from common.Address, contractName string) (*types.Transaction, error) { + contract := gen.Contracts[contractName] + if contract == nil { + return nil, errors.New("No contract with name " + contractName) + } + return gen.createTx(from, nil, big.NewInt(0), 1000000, contract.DeploymentCode) +} + +func (gen *GenContext) CreateCallTx(from common.Address, to common.Address, methodName string, args ...interface{}) (*types.Transaction, error) { + contractName, ok := gen.deployed[to] + if !ok { + return nil, errors.New("No contract deployed at address " + to.String()) + } + contract := gen.Contracts[contractName] + if contract == nil { + return nil, errors.New("No contract with name " + contractName) + } + + packed, err := contract.ABI.Pack(methodName, args...) + if err != nil { + panic(err) + } + return gen.createTx(from, &to, big.NewInt(0), 100000, packed) +} + +func (gen *GenContext) DeployContract(from common.Address, contractName string) (common.Address, error) { + tx, err := gen.CreateContractTx(from, contractName) + if err != nil { + return common.Address{}, err + } + addr := crypto.CreateAddress(from, gen.block.TxNonce(from)) + gen.deployed[addr] = contractName + gen.block.AddTx(tx) + return addr, nil +} + +func (gen *GenContext) createTx(from common.Address, to *common.Address, amount *big.Int, gasLimit uint64, data []byte) (*types.Transaction, error) { + signer := types.MakeSigner(gen.ChainConfig, gen.block.Number(), gen.time) + nonce := gen.block.TxNonce(from) + priv, ok := gen.Keys[from] + if !ok { + return nil, errors.New("No private key for sender address" + from.String()) + } + + var tx *types.Transaction + if gen.ChainConfig.IsLondon(gen.block.Number()) { + tx = types.NewTx(&types.DynamicFeeTx{ + ChainID: gen.ChainConfig.ChainID, + Nonce: nonce, + To: to, + Gas: gasLimit, + GasTipCap: big.NewInt(50), + GasFeeCap: big.NewInt(1000000000), + Value: amount, + Data: data, + }) + } else { + tx = types.NewTx(&types.LegacyTx{ + Nonce: nonce, + To: to, + Value: amount, + Gas: gasLimit, + Data: data, + }) + } + return types.SignTx(tx, signer, priv) +} diff --git a/test_helpers/db.go b/test_helpers/db.go new file mode 100644 index 0000000..964b9ce --- /dev/null +++ b/test_helpers/db.go @@ -0,0 +1,33 @@ +package test_helpers + +import ( + "fmt" + + "github.com/jmoiron/sqlx" +) + +// ClearDB is used to empty the IPLD-ETH tables after tests +func ClearDB(db *sqlx.DB) error { + tx, err := db.Beginx() + if err != nil { + return err + } + statements := []string{ + `TRUNCATE nodes`, + `TRUNCATE ipld.blocks`, + `TRUNCATE eth.header_cids`, + `TRUNCATE eth.uncle_cids`, + `TRUNCATE eth.transaction_cids`, + `TRUNCATE eth.receipt_cids`, + `TRUNCATE eth.state_cids`, + `TRUNCATE eth.storage_cids`, + `TRUNCATE eth.log_cids`, + `TRUNCATE eth_meta.watched_addresses`, + } + for _, stm := range statements { + if _, err = tx.Exec(stm); err != nil { + return fmt.Errorf("error executing `%s`: %w", stm, err) + } + } + return tx.Commit() +} diff --git a/test_helpers/indexing.go b/test_helpers/indexing.go new file mode 100644 index 0000000..5c34b58 --- /dev/null +++ b/test_helpers/indexing.go @@ -0,0 +1,100 @@ +package test_helpers + +import ( + "context" + "fmt" + "math/big" + + "github.com/cerc-io/plugeth-statediff" + "github.com/cerc-io/plugeth-statediff/adapt" + "github.com/cerc-io/plugeth-statediff/indexer" + "github.com/cerc-io/plugeth-statediff/indexer/interfaces" + "github.com/cerc-io/plugeth-statediff/indexer/node" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +type IndexChainParams struct { + Blocks []*types.Block + Receipts []types.Receipts + StateCache state.Database + + StateDiffParams statediff.Params + TotalDifficulty *big.Int + // Whether to skip indexing state nodes (state_cids, storage_cids) + SkipStateNodes bool + // Whether to skip indexing IPLD blocks + SkipIPLDs bool +} + +func NewIndexer(ctx context.Context, chainConfig *params.ChainConfig, genHash common.Hash, dbconfig interfaces.Config) (interfaces.StateDiffIndexer, error) { + testInfo := node.Info{ + GenesisBlock: genHash.String(), + NetworkID: "1", + ID: "1", + ClientName: "geth", + ChainID: chainConfig.ChainID.Uint64(), + } + _, indexer, err := indexer.NewStateDiffIndexer(ctx, chainConfig, testInfo, dbconfig, true) + return indexer, err +} + +func IndexChain(indexer interfaces.StateDiffIndexer, params IndexChainParams) error { + builder := statediff.NewBuilder(adapt.GethStateView(params.StateCache)) + // iterate over the blocks, generating statediff payloads, and transforming the data into Postgres + for i, block := range params.Blocks { + var args statediff.Args + var rcts types.Receipts + if i == 0 { + args = statediff.Args{ + OldStateRoot: common.Hash{}, + NewStateRoot: block.Root(), + BlockNumber: block.Number(), + BlockHash: block.Hash(), + } + } else { + args = statediff.Args{ + OldStateRoot: params.Blocks[i-1].Root(), + NewStateRoot: block.Root(), + BlockNumber: block.Number(), + BlockHash: block.Hash(), + } + rcts = params.Receipts[i-1] + } + + diff, err := builder.BuildStateDiffObject(args, params.StateDiffParams) + if err != nil { + return fmt.Errorf("failed to build diff (block %d): %w", block.Number(), err) + } + tx, err := indexer.PushBlock(block, rcts, params.TotalDifficulty) + if err != nil { + return fmt.Errorf("failed to index block (block %d): %w", block.Number(), err) + } + defer tx.RollbackOnFailure(err) + + if !params.SkipStateNodes { + for _, node := range diff.Nodes { + if err = indexer.PushStateNode(tx, node, block.Hash().String()); err != nil { + if err != nil { + return fmt.Errorf("failed to index state node: %w", err) + } + } + } + } + if !params.SkipIPLDs { + for _, ipld := range diff.IPLDs { + if err := indexer.PushIPLD(tx, ipld); err != nil { + if err != nil { + return fmt.Errorf("failed to index IPLD: %w", err) + } + } + } + } + if err = tx.Submit(); err != nil { + return fmt.Errorf("failed to commit diff: %w", err) + } + } + return nil +}