From 530aa2b62f98111f0be29eaf7fe6ba97bc616bbb Mon Sep 17 00:00:00 2001 From: Arijit Das Date: Tue, 18 Jan 2022 22:44:38 +0530 Subject: [PATCH] Add unit test. --- environments/example.toml | 2 +- pkg/validator/config.go | 5 ++ pkg/validator/validator.go | 105 +++++++++++++++++++++++-- test/validator_suite_test.go | 19 +++++ test/validator_test.go | 143 +++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 test/validator_suite_test.go create mode 100644 test/validator_test.go diff --git a/environments/example.toml b/environments/example.toml index 4ba392b..151fe06 100644 --- a/environments/example.toml +++ b/environments/example.toml @@ -7,4 +7,4 @@ [validate] block-height = 1 - trail = 3100 \ No newline at end of file + trail = 10 \ No newline at end of file diff --git a/pkg/validator/config.go b/pkg/validator/config.go index 2c102da..5409541 100644 --- a/pkg/validator/config.go +++ b/pkg/validator/config.go @@ -2,12 +2,17 @@ package validator import ( "fmt" + "math/big" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/statediff/indexer/node" "github.com/ethereum/go-ethereum/statediff/indexer/postgres" "github.com/spf13/viper" ) +var TestChainConfig = ¶ms.ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(100), big.NewInt(0), nil, nil, new(params.EthashConfig), nil} + type Config struct { dbParams postgres.ConnectionParams dbConfig postgres.ConnectionConfig diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index a7e5d97..72c3e65 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -4,8 +4,13 @@ import ( "context" "errors" "fmt" + "math/big" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/clique" + "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" @@ -21,6 +26,12 @@ import ( ethServerShared "github.com/vulcanize/ipld-eth-server/pkg/shared" ) +var ( + big8 = big.NewInt(8) + big32 = big.NewInt(32) + testHash = common.HexToHash("0x1283a0bca5cce009bcf3e5a860eccdc202d1345f464024f2ee8ea1e1254349e7") +) + type service struct { db *postgres.DB blockNum, trail uint64 @@ -85,7 +96,7 @@ func NewDB(connectString string, config postgres.ConnectionConfig, node node.Inf // Start is used to begin the service func (s *service) Start(ctx context.Context) (uint64, error) { - api, err := ethAPI(s.db) + api, err := ethAPI(ctx, s.db) if err != nil { return 0, err } @@ -106,6 +117,7 @@ func (s *service) Start(ctx context.Context) (uint64, error) { } blockStateRoot := validateBlock.Header().Root.String() + dbStateRoot := stateDB.IntermediateRoot(true).String() if blockStateRoot != dbStateRoot { s.logger.Errorf("failed to verify state root at block %d", idxBlockNum) @@ -129,10 +141,9 @@ func (s *service) Start(ctx context.Context) (uint64, error) { return idxBlockNum, nil } -func ethAPI(db *postgres.DB) (*ipldEth.PublicEthAPI, error) { - // TODO: decide network for chainConfig. +func ethAPI(ctx context.Context, db *postgres.DB) (*ipldEth.PublicEthAPI, error) { + // TODO: decide network for custom chainConfig. backend, err := NewEthBackend(db, &ipldEth.Config{ - ChainConfig: params.RinkebyChainConfig, GroupCacheConfig: ðServerShared.GroupCacheConfig{ StateDB: ethServerShared.GroupConfig{ Name: "vulcanize_validator", @@ -144,6 +155,16 @@ func ethAPI(db *postgres.DB) (*ipldEth.PublicEthAPI, error) { return nil, err } + var genesisBlock *types.Block + if backend.Config.ChainConfig == nil { + genesisBlock, err = backend.BlockByNumber(ctx, rpc.BlockNumber(0)) + if err != nil { + return nil, err + } + + backend.Config.ChainConfig = setChainConfig(genesisBlock.Hash()) + } + return ipldEth.NewPublicEthAPI(backend, nil, false, false, false) } @@ -177,7 +198,7 @@ func applyTransaction(block *types.Block, backend *ipldEth.Backend) (*state.Stat // Assemble the transaction call message and return if the requested offset msg, _ := tx.AsMessage(signer, block.BaseFee()) txContext := core.NewEVMTxContext(msg) - ctx := core.NewEVMBlockContext(block.Header(), backend, nil) + ctx := core.NewEVMBlockContext(block.Header(), backend, getAuthor(backend, block.Header())) // Not yet the searched for transaction, execute on top of the current state newEVM := vm.NewEVM(ctx, txContext, stateDB, backend.Config.ChainConfig, vm.Config{}) @@ -187,5 +208,79 @@ func applyTransaction(block *types.Block, backend *ipldEth.Backend) (*state.Stat return nil, fmt.Errorf("transaction %#x failed: %w", tx.Hash(), err) } } + + if backend.Config.ChainConfig.Ethash != nil { + accumulateRewards(backend.Config.ChainConfig, stateDB, block.Header(), block.Uncles()) + } + return stateDB, nil } + +// accumulateRewards credits the coinbase of the given block with the mining +// reward. The total reward consists of the static block reward and rewards for +// included uncles. The coinbase of each uncle block is also rewarded. +func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) { + // Select the correct block reward based on chain progression + blockReward := ethash.FrontierBlockReward + if config.IsByzantium(header.Number) { + blockReward = ethash.ByzantiumBlockReward + } + + if config.IsConstantinople(header.Number) { + blockReward = ethash.ConstantinopleBlockReward + } + + // Accumulate the rewards for the miner and any included uncles + reward := new(big.Int).Set(blockReward) + r := new(big.Int) + for _, uncle := range uncles { + r.Add(uncle.Number, big8) + r.Sub(r, header.Number) + r.Mul(r, blockReward) + r.Div(r, big8) + state.AddBalance(uncle.Coinbase, r) + + r.Div(blockReward, big32) + reward.Add(reward, r) + } + + state.AddBalance(header.Coinbase, reward) +} + +func setChainConfig(ghash common.Hash) *params.ChainConfig { + switch { + case ghash == params.MainnetGenesisHash: + return params.MainnetChainConfig + case ghash == params.RopstenGenesisHash: + return params.RopstenChainConfig + case ghash == params.SepoliaGenesisHash: + return params.SepoliaChainConfig + case ghash == params.RinkebyGenesisHash: + return params.RinkebyChainConfig + case ghash == params.GoerliGenesisHash: + return params.GoerliChainConfig + case ghash == testHash: + return TestChainConfig + default: + return params.AllEthashProtocolChanges + } +} + +func getAuthor(b *ipldEth.Backend, header *types.Header) *common.Address { + author, err := getEngine(b).Author(header) + if err != nil { + return nil + } + + return &author +} + +func getEngine(b *ipldEth.Backend) consensus.Engine { + // TODO: add logic for other engines + if b.Config.ChainConfig.Clique != nil { + engine := clique.New(b.Config.ChainConfig.Clique, nil) + return engine + } + + return ethash.NewFaker() +} diff --git a/test/validator_suite_test.go b/test/validator_suite_test.go new file mode 100644 index 0000000..8e6be66 --- /dev/null +++ b/test/validator_suite_test.go @@ -0,0 +1,19 @@ +package test + +import ( + "io/ioutil" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" +) + +func TestETHSuite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "eth ipld validator eth suite test") +} + +var _ = BeforeSuite(func() { + logrus.SetOutput(ioutil.Discard) +}) diff --git a/test/validator_test.go b/test/validator_test.go new file mode 100644 index 0000000..51fc102 --- /dev/null +++ b/test/validator_test.go @@ -0,0 +1,143 @@ +package test + +import ( + "context" + "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" + "github.com/ethereum/go-ethereum/statediff" + "github.com/ethereum/go-ethereum/statediff/indexer" + "github.com/ethereum/go-ethereum/statediff/indexer/node" + "github.com/ethereum/go-ethereum/statediff/indexer/postgres" + sdtypes "github.com/ethereum/go-ethereum/statediff/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/vulcanize/ipld-eth-server/pkg/eth" + + "github.com/vulcanize/ipld-eth-server/pkg/eth/test_helpers" + + "github.com/Vulcanize/ipld-eth-db-validator/pkg/validator" +) + +const ( + chainLength = 20 + blockHeight = 1 + trail = 15 +) + +// SetupDB is use to setup a db for watcher tests +func setupDB() (*postgres.DB, error) { + uri := postgres.DbConnectionString(postgres.ConnectionParams{ + User: "vdbm", + Password: "password", + Hostname: "localhost", + Name: "vulcanize_public", + Port: 5432, + }) + return postgres.NewDB(uri, postgres.ConnectionConfig{}, node.Info{}) +} + +var _ = Describe("eth state reading tests", func() { + var ( + blocks []*types.Block + receipts []types.Receipts + chain *core.BlockChain + db *postgres.DB + chainConfig = params.TestChainConfig + mockTD = big.NewInt(1337) + err error + ) + + It("test init", func() { + db, err = setupDB() + Expect(err).ToNot(HaveOccurred()) + + transformer, err := indexer.NewStateDiffIndexer(chainConfig, db) + Expect(err).ToNot(HaveOccurred()) + + // make the test blockchain (and state) + blocks, receipts, chain = test_helpers.MakeChain(chainLength, test_helpers.Genesis, test_helpers.TestChainGen) + params := statediff.Params{ + IntermediateStateNodes: true, + IntermediateStorageNodes: true, + } + + // iterate over the blocks, generating statediff payloads, and transforming the data into Postgres + builder := statediff.NewBuilder(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()) + + for _, node := range diff.Nodes { + err = transformer.PushStateNode(tx, node) + Expect(err).ToNot(HaveOccurred()) + } + + err = tx.Close(err) + Expect(err).ToNot(HaveOccurred()) + } + + // Insert some non-canonical data into the database so that we test our ability to discern canonicity + indexAndPublisher, err := indexer.NewStateDiffIndexer(chainConfig, db) + Expect(err).ToNot(HaveOccurred()) + + tx, err := indexAndPublisher.PushBlock(test_helpers.MockBlock, test_helpers.MockReceipts, test_helpers.MockBlock.Difficulty()) + Expect(err).ToNot(HaveOccurred()) + + err = tx.Close(err) + Expect(err).ToNot(HaveOccurred()) + + // The non-canonical header has a child + tx, err = indexAndPublisher.PushBlock(test_helpers.MockChild, test_helpers.MockReceipts, test_helpers.MockChild.Difficulty()) + Expect(err).ToNot(HaveOccurred()) + + hash := sdtypes.CodeAndCodeHash{ + Hash: test_helpers.CodeHash, + Code: test_helpers.ContractCode, + } + + err = indexAndPublisher.PushCodeAndCodeHash(tx, hash) + Expect(err).ToNot(HaveOccurred()) + + err = tx.Close(err) + Expect(err).ToNot(HaveOccurred()) + }) + + defer It("test teardown", func() { + eth.TearDownDB(db) + chain.Stop() + }) + + Describe("state_validation", func() { + It("Validator", func() { + srvc := validator.NewService(db, blockHeight, trail) + + _, err := srvc.Start(context.Background()) + Expect(err).ToNot(HaveOccurred()) + }) + }) +})