diff --git a/pkg/eth/debug_test/debug_test.go b/pkg/eth/debug_test/debug_test.go new file mode 100644 index 00000000..2efa0338 --- /dev/null +++ b/pkg/eth/debug_test/debug_test.go @@ -0,0 +1,423 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package eth_debug_test + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/json" + "errors" + "fmt" + "math/big" + "sort" + "time" + + statediff "github.com/cerc-io/plugeth-statediff" + "github.com/cerc-io/plugeth-statediff/adapt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "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/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "github.com/jmoiron/sqlx" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/cerc-io/ipld-eth-server/v5/pkg/eth" + "github.com/cerc-io/ipld-eth-server/v5/pkg/shared" +) + +var ( + db *sqlx.DB + chainConfig = &*params.TestChainConfig + mockTD = big.NewInt(1337) + ctx = context.Background() + tb *testBackend + accounts Accounts + genBlocks int +) + +var _ = BeforeSuite(func() { + chainConfig.LondonBlock = big.NewInt(100) + + // db and type initializations + var err error + db = shared.SetupDB() + + // Initialize test accounts + accounts = newAccounts(3) + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks = 10 + signer := types.HomesteadSigner{} + tb, blocks, receipts := newTestBackend(genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[1].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[0].key) + b.AddTx(tx) + }) + transformer := shared.SetupTestStateDiffIndexer(ctx, chainConfig, blocks[0].Hash()) + params := statediff.Params{} + + // iterate over the blocks, generating statediff payloads, and transforming the data into Postgres + builder := statediff.NewBuilder(adapt.GethStateView(tb.chain.StateCache())) + for i, block := range 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: blocks[i-1].Root(), + NewStateRoot: block.Root(), + BlockNumber: block.Number(), + BlockHash: block.Hash(), + } + rcts = receipts[i-1] + } + diff, err := builder.BuildStateDiffObject(args, params) + Expect(err).ToNot(HaveOccurred()) + tx, err := transformer.PushBlock(block, rcts, mockTD) + Expect(err).ToNot(HaveOccurred()) + defer tx.RollbackOnFailure(err) + + for _, node := range diff.Nodes { + err = transformer.PushStateNode(tx, node, block.Hash().String()) + Expect(err).ToNot(HaveOccurred()) + } + + for _, ipld := range diff.IPLDs { + err = transformer.PushIPLD(tx, ipld) + Expect(err).ToNot(HaveOccurred()) + } + + err = tx.Submit() + Expect(err).ToNot(HaveOccurred()) + } + + backend, err := eth.NewEthBackend(db, ð.Config{ + ChainConfig: chainConfig, + VMConfig: vm.Config{}, + RPCGasCap: big.NewInt(10000000000), // Max gas capacity for a rpc call. + GroupCacheConfig: &shared.GroupCacheConfig{ + StateDB: shared.GroupConfig{ + Name: "eth_state_test", + CacheSizeInMB: 8, + CacheExpiryInMins: 60, + LogStatsIntervalInSecs: 0, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + + tracingAPI, _ = eth.NewTracingAPI(backend, nil, eth.APIConfig{StateDiffTimeout: shared.DefaultStateDiffTimeout}) + +}) + +var _ = AfterSuite(func() { + shared.TearDownDB(db) + tb.teardown() +}) + +var ( + tracingAPI *eth.TracingAPI +) + +var _ = Describe("eth state reading tests", func() { + + Describe("debug_traceCall", func() { + It("", func() { + var testSuite = []struct { + blockNumber rpc.BlockNumber + call eth.TransactionArgs + config *eth.TraceCallConfig + expectErr error + expect string + }{ + // Standard JSON trace upon the genesis, plain transfer. + { + blockNumber: rpc.BlockNumber(0), + call: eth.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: nil, + expect: `{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}`, + }, + // Standard JSON trace upon the head, plain transfer. + { + blockNumber: rpc.BlockNumber(genBlocks), + call: eth.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: nil, + expect: `{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}`, + }, + // Standard JSON trace upon the non-existent block, error expects + { + blockNumber: rpc.BlockNumber(genBlocks + 1), + call: eth.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: fmt.Errorf("block #%d not found", genBlocks+1), + //expect: nil, + }, + // Standard JSON trace upon the latest block + { + blockNumber: rpc.LatestBlockNumber, + call: eth.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: nil, + expect: `{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}`, + }, + // Tracing on 'pending' should fail: + { + blockNumber: rpc.PendingBlockNumber, + call: eth.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: fmt.Errorf("tracing on top of pending is not supported"), + }, + { + blockNumber: rpc.LatestBlockNumber, + call: eth.TransactionArgs{ + From: &accounts[0].addr, + Input: &hexutil.Bytes{0x43}, // blocknumber + }, + config: ð.TraceCallConfig{ + BlockOverrides: ð.BlockOverrides{Number: (*hexutil.Big)(big.NewInt(0x1337))}, + }, + expectErr: nil, + expect: ` {"gas":53018,"failed":false,"returnValue":"","structLogs":[ + {"pc":0,"op":"NUMBER","gas":24946984,"gasCost":2,"depth":1,"stack":[]}, + {"pc":1,"op":"STOP","gas":24946982,"gasCost":0,"depth":1,"stack":["0x1337"]}]}`, + }, + } + for _, testspec := range testSuite { + result, err := tracingAPI.TraceCall(context.Background(), testspec.call, rpc.BlockNumberOrHash{BlockNumber: &testspec.blockNumber}, testspec.config) + if testspec.expectErr != nil { + Expect(err).To(HaveOccurred()) + Expect(err).To(Equal(testspec.expectErr)) + } else { + Expect(err).ToNot(HaveOccurred()) + var have *logger.ExecutionResult + err := json.Unmarshal(result.(json.RawMessage), &have) + Expect(err).ToNot(HaveOccurred()) + var want *logger.ExecutionResult + err = json.Unmarshal([]byte(testspec.expect), &want) + Expect(err).ToNot(HaveOccurred()) + Expect(have).To(Equal(want)) + } + } + }) + }) +}) + +type testBackend struct { + chainConfig *params.ChainConfig + engine consensus.Engine + chaindb ethdb.Database + chain *core.BlockChain + + refHook func() // Hook is invoked when the requested state is referenced + relHook func() // Hook is invoked when the requested state is released +} + +func (b *testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + return b.chain.GetHeaderByHash(hash), nil +} + +func (b *testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) { + if number == rpc.PendingBlockNumber || number == rpc.LatestBlockNumber { + return b.chain.CurrentHeader(), nil + } + return b.chain.GetHeaderByNumber(uint64(number)), nil +} + +func (b *testBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return b.chain.GetBlockByHash(hash), nil +} + +func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) { + if number == rpc.PendingBlockNumber || number == rpc.LatestBlockNumber { + return b.chain.GetBlockByNumber(b.chain.CurrentBlock().Number.Uint64()), nil + } + return b.chain.GetBlockByNumber(uint64(number)), nil +} + +func (b *testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + tx, hash, blockNumber, index := rawdb.ReadTransaction(b.chaindb, txHash) + return tx, hash, blockNumber, index, nil +} + +func (b *testBackend) RPCGasCap() uint64 { + return 25000000 +} + +func (b *testBackend) ChainConfig() *params.ChainConfig { + return b.chainConfig +} + +func (b *testBackend) Engine() consensus.Engine { + return b.engine +} + +func (b *testBackend) ChainDb() ethdb.Database { + return b.chaindb +} + +// teardown releases the associated resources. +func (b *testBackend) teardown() { + b.chain.Stop() +} + +var ( + errStateNotFound = errors.New("state not found") + errBlockNotFound = errors.New("block not found") +) + +func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) { + statedb, err := b.chain.StateAt(block.Root()) + if err != nil { + return nil, nil, errStateNotFound + } + if b.refHook != nil { + b.refHook() + } + release := func() { + if b.relHook != nil { + b.relHook() + } + } + return statedb, release, nil +} + +func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { + parent := b.chain.GetBlock(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, vm.BlockContext{}, nil, nil, errBlockNotFound + } + statedb, release, err := b.StateAtBlock(ctx, parent, reexec, nil, true, false) + if err != nil { + return nil, vm.BlockContext{}, nil, nil, errStateNotFound + } + if txIndex == 0 && len(block.Transactions()) == 0 { + return nil, vm.BlockContext{}, statedb, release, nil + } + // Recompute transactions up to the target index. + signer := types.MakeSigner(b.chainConfig, block.Number()) + for idx, tx := range block.Transactions() { + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) + txContext := core.NewEVMTxContext(msg) + context := core.NewEVMBlockContext(block.Header(), b.chain, nil) + if idx == txIndex { + return msg, context, statedb, release, nil + } + vmenv := vm.NewEVM(context, txContext, statedb, b.chainConfig, vm.Config{}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) + } + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + } + return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) +} + +// testBackend creates a new test backend. OBS: After test is done, teardown must be +// invoked in order to release associated resources. +func newTestBackend(n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) (*testBackend, types.Blocks, []types.Receipts) { + backend := &testBackend{ + chainConfig: gspec.Config, + engine: ethash.NewFaker(), + chaindb: rawdb.NewMemoryDatabase(), + } + // Generate blocks for testing + _, blocks, receipts := core.GenerateChainWithGenesis(gspec, backend.engine, n, generator) + + // Import the canonical chain + cacheConfig := &core.CacheConfig{ + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 0, + TrieDirtyDisabled: true, // Archive mode + } + chain, err := core.NewBlockChain(backend.chaindb, cacheConfig, gspec, nil, backend.engine, vm.Config{}, nil, nil) + Expect(err).ToNot(HaveOccurred()) + n, err = chain.InsertChain(blocks) + Expect(err).ToNot(HaveOccurred()) + backend.chain = chain + return backend, blocks, receipts +} + +type Account struct { + key *ecdsa.PrivateKey + addr common.Address +} + +type Accounts []Account + +func (a Accounts) Len() int { return len(a) } +func (a Accounts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Accounts) Less(i, j int) bool { return bytes.Compare(a[i].addr.Bytes(), a[j].addr.Bytes()) < 0 } + +func newAccounts(n int) (accounts Accounts) { + for i := 0; i < n; i++ { + key, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(key.PublicKey) + accounts = append(accounts, Account{key: key, addr: addr}) + } + sort.Sort(accounts) + return accounts +} diff --git a/pkg/eth/debug_test/eth_suite_test.go b/pkg/eth/debug_test/eth_suite_test.go new file mode 100644 index 00000000..5a5143c0 --- /dev/null +++ b/pkg/eth/debug_test/eth_suite_test.go @@ -0,0 +1,29 @@ +// VulcanizeDB +// Copyright © 2019 Vulcanize + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package eth_debug_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestETHSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ipld-eth-server/pkg/eth/state_test") +}