diff --git a/db/migrations/1514392444_add_miner.down.sql b/db/migrations/1514392444_add_miner.down.sql new file mode 100644 index 00000000..417ad684 --- /dev/null +++ b/db/migrations/1514392444_add_miner.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE blocks + DROP COLUMN block_miner; \ No newline at end of file diff --git a/db/migrations/1514392444_add_miner.up.sql b/db/migrations/1514392444_add_miner.up.sql new file mode 100644 index 00000000..2667365b --- /dev/null +++ b/db/migrations/1514392444_add_miner.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE blocks + ADD COLUMN block_miner VARCHAR(42); \ No newline at end of file diff --git a/db/migrations/1514397430_add_extra_data.down.sql b/db/migrations/1514397430_add_extra_data.down.sql new file mode 100644 index 00000000..0da3cdeb --- /dev/null +++ b/db/migrations/1514397430_add_extra_data.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE blocks + DROP COLUMN block_extra_data; \ No newline at end of file diff --git a/db/migrations/1514397430_add_extra_data.up.sql b/db/migrations/1514397430_add_extra_data.up.sql new file mode 100644 index 00000000..71839271 --- /dev/null +++ b/db/migrations/1514397430_add_extra_data.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE blocks + ADD COLUMN block_extra_data VARCHAR; \ No newline at end of file diff --git a/db/migrations/1514476024_add_block_reward_uncle_reward.down.sql b/db/migrations/1514476024_add_block_reward_uncle_reward.down.sql new file mode 100644 index 00000000..ec96263f --- /dev/null +++ b/db/migrations/1514476024_add_block_reward_uncle_reward.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE blocks + DROP COLUMN block_reward, + DROP COLUMN block_uncles_reward; diff --git a/db/migrations/1514476024_add_block_reward_uncle_reward.up.sql b/db/migrations/1514476024_add_block_reward_uncle_reward.up.sql new file mode 100644 index 00000000..447e53a6 --- /dev/null +++ b/db/migrations/1514476024_add_block_reward_uncle_reward.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE blocks + ADD COLUMN block_reward NUMERIC, + ADD COLUMN block_uncles_reward NUMERIC; diff --git a/db/schema.sql b/db/schema.sql index 76ac4374..b3eae30a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -51,7 +51,11 @@ CREATE TABLE blocks ( block_size bigint, uncle_hash character varying(66), node_id integer NOT NULL, - is_final boolean + is_final boolean, + block_miner character varying(42), + block_extra_data character varying, + block_reward numeric, + block_uncles_reward numeric ); diff --git a/integration_test/block_rewards_test.go b/integration_test/block_rewards_test.go new file mode 100644 index 00000000..b5ee5351 --- /dev/null +++ b/integration_test/block_rewards_test.go @@ -0,0 +1,34 @@ +package integration + +import ( + "log" + + cfg "github.com/8thlight/vulcanizedb/pkg/config" + "github.com/8thlight/vulcanizedb/pkg/geth" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Rewards calculations", func() { + + It("calculates a block reward for a real block", func() { + config, err := cfg.NewConfig("infura") + if err != nil { + log.Fatalln(err) + } + blockchain := geth.NewGethBlockchain(config.Client.IPCPath) + block := blockchain.GetBlockByNumber(1071819) + Expect(block.Reward).To(Equal(5.31355)) + }) + + It("calculates an uncle reward for a real block", func() { + config, err := cfg.NewConfig("infura") + if err != nil { + log.Fatalln(err) + } + blockchain := geth.NewGethBlockchain(config.Client.IPCPath) + block := blockchain.GetBlockByNumber(1071819) + Expect(block.UnclesReward).To(Equal(6.875)) + }) + +}) diff --git a/pkg/core/block.go b/pkg/core/block.go index 6e9f5e53..d14f43b7 100644 --- a/pkg/core/block.go +++ b/pkg/core/block.go @@ -1,10 +1,14 @@ package core type Block struct { + Reward float64 Difficulty int64 + ExtraData string GasLimit int64 GasUsed int64 Hash string + IsFinal bool + Miner string Nonce string Number int64 ParentHash string @@ -12,5 +16,5 @@ type Block struct { Time int64 Transactions []Transaction UncleHash string - IsFinal bool + UnclesReward float64 } diff --git a/pkg/geth/geth_block_rewards.go b/pkg/geth/geth_block_rewards.go new file mode 100644 index 00000000..f491041d --- /dev/null +++ b/pkg/geth/geth_block_rewards.go @@ -0,0 +1,58 @@ +package geth + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func CalcUnclesReward(gethBlock *types.Block) float64 { + var unclesReward float64 + for _, uncle := range gethBlock.Uncles() { + blockNumber := gethBlock.Number().Int64() + staticBlockReward := float64(staticRewardByBlockNumber(blockNumber)) + unclesReward += (1.0 + float64(uncle.Number.Int64()-gethBlock.Number().Int64())/8.0) * staticBlockReward + } + return unclesReward +} + +func CalcBlockReward(gethBlock *types.Block, client GethClient) float64 { + blockNumber := gethBlock.Number().Int64() + staticBlockReward := staticRewardByBlockNumber(blockNumber) + transactionFees := calcTransactionFees(gethBlock, client) + uncleInclusionRewards := calcUncleInclusionRewards(gethBlock) + return transactionFees + uncleInclusionRewards + staticBlockReward +} + +func calcUncleInclusionRewards(gethBlock *types.Block) float64 { + var uncleInclusionRewards float64 + staticBlockReward := staticRewardByBlockNumber(gethBlock.Number().Int64()) + for range gethBlock.Uncles() { + uncleInclusionRewards += staticBlockReward * 1 / 32 + } + return uncleInclusionRewards +} + +func calcTransactionFees(gethBlock *types.Block, client GethClient) float64 { + var transactionFees float64 + for _, transaction := range gethBlock.Transactions() { + receipt, err := client.TransactionReceipt(context.Background(), transaction.Hash()) + if err != nil { + continue + } + transactionFees += float64(transaction.GasPrice().Int64() * receipt.GasUsed.Int64()) + } + return transactionFees / params.Ether +} + +func staticRewardByBlockNumber(blockNumber int64) float64 { + var staticBlockReward float64 + //https://blog.ethereum.org/2017/10/12/byzantium-hf-announcement/ + if blockNumber >= 4370000 { + staticBlockReward = 3 + } else { + staticBlockReward = 5 + } + return staticBlockReward +} diff --git a/pkg/geth/geth_block_to_core_block.go b/pkg/geth/geth_block_to_core_block.go index 4aa67bfc..ece19b56 100644 --- a/pkg/geth/geth_block_to_core_block.go +++ b/pkg/geth/geth_block_to_core_block.go @@ -12,6 +12,7 @@ import ( type GethClient interface { TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) + TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) } func GethBlockToCoreBlock(gethBlock *types.Block, client GethClient) core.Block { @@ -21,18 +22,24 @@ func GethBlockToCoreBlock(gethBlock *types.Block, client GethClient) core.Block transaction := gethTransToCoreTrans(gethTransaction, &from) transactions = append(transactions, transaction) } + blockReward := CalcBlockReward(gethBlock, client) + uncleReward := CalcUnclesReward(gethBlock) return core.Block{ Difficulty: gethBlock.Difficulty().Int64(), + ExtraData: hexutil.Encode(gethBlock.Extra()), GasLimit: gethBlock.GasLimit().Int64(), GasUsed: gethBlock.GasUsed().Int64(), Hash: gethBlock.Hash().Hex(), + Miner: gethBlock.Coinbase().Hex(), Nonce: hexutil.Encode(gethBlock.Header().Nonce[:]), Number: gethBlock.Number().Int64(), ParentHash: gethBlock.ParentHash().Hex(), + Reward: blockReward, Size: gethBlock.Size().Int64(), Time: gethBlock.Time().Int64(), Transactions: transactions, UncleHash: gethBlock.UncleHash().Hex(), + UnclesReward: uncleReward, } } diff --git a/pkg/geth/geth_block_to_core_block_test.go b/pkg/geth/geth_block_to_core_block_test.go index 25ec216d..5d551a6a 100644 --- a/pkg/geth/geth_block_to_core_block_test.go +++ b/pkg/geth/geth_block_to_core_block_test.go @@ -13,7 +13,28 @@ import ( . "github.com/onsi/gomega" ) -type FakeGethClient struct{} +type FakeGethClient struct { + receipts map[string]*types.Receipt +} + +func NewFakeClient() *FakeGethClient { + return &FakeGethClient{ + receipts: make(map[string]*types.Receipt), + } +} + +func (client *FakeGethClient) AddReceipts(receipts []*types.Receipt) { + for _, receipt := range receipts { + client.receipts[receipt.TxHash.Hex()] = receipt + } +} + +func (client *FakeGethClient) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + if gasUsed, ok := client.receipts[txHash.Hex()]; ok { + return gasUsed, nil + } + return &types.Receipt{GasUsed: big.NewInt(0)}, nil +} func (client *FakeGethClient) TransactionSender(ctx context.Context, tx *types.Transaction, block common.Hash, index uint) (common.Address, error) { return common.HexToAddress("0x123"), nil @@ -21,10 +42,12 @@ func (client *FakeGethClient) TransactionSender(ctx context.Context, tx *types.T var _ = Describe("Conversion of GethBlock to core.Block", func() { - It("converts basic Block metada", func() { + It("converts basic Block metadata", func() { difficulty := big.NewInt(1) gasLimit := int64(100000) gasUsed := int64(100000) + miner := common.HexToAddress("0x0000000000000000000000000000000000000123") + extraData, _ := hexutil.Decode("0xe4b883e5bda9e7a59ee4bb99e9b1bc") nonce := types.BlockNonce{10} number := int64(1) time := int64(140000000) @@ -33,6 +56,8 @@ var _ = Describe("Conversion of GethBlock to core.Block", func() { Difficulty: difficulty, GasLimit: big.NewInt(gasLimit), GasUsed: big.NewInt(gasUsed), + Extra: extraData, + Coinbase: miner, Nonce: nonce, Number: big.NewInt(number), ParentHash: common.Hash{64}, @@ -45,18 +70,123 @@ var _ = Describe("Conversion of GethBlock to core.Block", func() { Expect(gethBlock.Difficulty).To(Equal(difficulty.Int64())) Expect(gethBlock.GasLimit).To(Equal(gasLimit)) + Expect(gethBlock.Miner).To(Equal(miner.Hex())) Expect(gethBlock.GasUsed).To(Equal(gasUsed)) Expect(gethBlock.Hash).To(Equal(block.Hash().Hex())) Expect(gethBlock.Nonce).To(Equal(hexutil.Encode(header.Nonce[:]))) Expect(gethBlock.Number).To(Equal(number)) Expect(gethBlock.ParentHash).To(Equal(block.ParentHash().Hex())) + Expect(gethBlock.ExtraData).To(Equal(hexutil.Encode(block.Extra()))) Expect(gethBlock.Size).To(Equal(block.Size().Int64())) Expect(gethBlock.Time).To(Equal(time)) Expect(gethBlock.UncleHash).To(Equal(block.UncleHash().Hex())) Expect(gethBlock.IsFinal).To(BeFalse()) }) - Describe("the converted transations", func() { + Describe("The block and uncle rewards calculations", func() { + It("calculates block rewards for a block", func() { + transaction := types.NewTransaction( + uint64(226823), + common.HexToAddress("0x108fedb097c1dcfed441480170144d8e19bb217f"), + big.NewInt(1080900090000000000), + big.NewInt(90000), + big.NewInt(50000000000), + []byte{}, + ) + transactions := []*types.Transaction{transaction} + + txHash := transaction.Hash() + receipt := types.Receipt{TxHash: txHash, GasUsed: big.NewInt(21000)} + receipts := []*types.Receipt{&receipt} + + number := int64(1071819) + header := types.Header{ + Number: big.NewInt(number), + } + uncles := []*types.Header{{Number: big.NewInt(1071817)}, {Number: big.NewInt(1071818)}} + block := types.NewBlock(&header, transactions, uncles, []*types.Receipt{}) + + client := NewFakeClient() + client.AddReceipts(receipts) + + Expect(geth.CalcBlockReward(block, client)).To(Equal(5.31355)) + }) + + It("calculates the uncles reward for a block", func() { + transaction := types.NewTransaction( + uint64(226823), + common.HexToAddress("0x108fedb097c1dcfed441480170144d8e19bb217f"), + big.NewInt(1080900090000000000), + big.NewInt(90000), + big.NewInt(50000000000), + []byte{}) + transactions := []*types.Transaction{transaction} + + receipt := types.Receipt{ + TxHash: transaction.Hash(), + GasUsed: big.NewInt(21000), + } + receipts := []*types.Receipt{&receipt} + + header := types.Header{ + Number: big.NewInt(int64(1071819)), + } + uncles := []*types.Header{ + {Number: big.NewInt(1071816)}, + {Number: big.NewInt(1071817)}, + } + block := types.NewBlock(&header, transactions, uncles, []*types.Receipt{}) + + client := NewFakeClient() + client.AddReceipts(receipts) + + Expect(geth.CalcUnclesReward(block)).To(Equal(6.875)) + }) + + It("decreases the static block reward from 5 to 3 for blocks after block 4,269,999", func() { + transactionOne := types.NewTransaction( + uint64(8072), + common.HexToAddress("0xebd17720aeb7ac5186c5dfa7bafeb0bb14c02551 "), + big.NewInt(0), + big.NewInt(500000), + big.NewInt(42000000000), + []byte{}, + ) + + transactionTwo := types.NewTransaction(uint64(8071), + common.HexToAddress("0x3cdab63d764c8c5048ed5e8f0a4e95534ba7e1ea"), + big.NewInt(0), + big.NewInt(500000), + big.NewInt(42000000000), + []byte{}) + + transactions := []*types.Transaction{transactionOne, transactionTwo} + + receiptOne := types.Receipt{ + TxHash: transactionOne.Hash(), + GasUsed: big.NewInt(297508), + } + receiptTwo := types.Receipt{ + TxHash: transactionTwo.Hash(), + GasUsed: big.NewInt(297508), + } + receipts := []*types.Receipt{&receiptOne, &receiptTwo} + + number := int64(4370055) + header := types.Header{ + Number: big.NewInt(number), + } + var uncles []*types.Header + block := types.NewBlock(&header, transactions, uncles, receipts) + + client := NewFakeClient() + client.AddReceipts(receipts) + + Expect(geth.CalcBlockReward(block, client)).To(Equal(3.024990672)) + }) + }) + + Describe("the converted transactions", func() { It("is empty", func() { header := types.Header{} block := types.NewBlock(&header, []*types.Transaction{}, []*types.Header{}, []*types.Receipt{}) @@ -66,7 +196,7 @@ var _ = Describe("Conversion of GethBlock to core.Block", func() { Expect(len(coreBlock.Transactions)).To(Equal(0)) }) - It("converts a single transations", func() { + It("converts a single transaction", func() { nonce := uint64(10000) header := types.Header{} to := common.Address{1} @@ -76,7 +206,9 @@ var _ = Describe("Conversion of GethBlock to core.Block", func() { payload := []byte("1234") gethTransaction := types.NewTransaction(nonce, to, amount, gasLimit, gasPrice, payload) - client := &FakeGethClient{} + + client := NewFakeClient() + gethBlock := types.NewBlock(&header, []*types.Transaction{gethTransaction}, []*types.Header{}, []*types.Receipt{}) coreBlock := geth.GethBlockToCoreBlock(gethBlock, client) @@ -94,8 +226,8 @@ var _ = Describe("Conversion of GethBlock to core.Block", func() { It("has an empty to field when transaction creates a new contract", func() { gethTransaction := types.NewContractCreation(uint64(10000), big.NewInt(10), big.NewInt(5000), big.NewInt(3), []byte("1234")) gethBlock := types.NewBlock(&types.Header{}, []*types.Transaction{gethTransaction}, []*types.Header{}, []*types.Receipt{}) - client := &FakeGethClient{} + client := NewFakeClient() coreBlock := geth.GethBlockToCoreBlock(gethBlock, client) coreTransaction := coreBlock.Transactions[0] diff --git a/pkg/repositories/postgres.go b/pkg/repositories/postgres.go index ebd2f9cb..c4b00e5b 100644 --- a/pkg/repositories/postgres.go +++ b/pkg/repositories/postgres.go @@ -198,7 +198,11 @@ func (repository Postgres) FindBlockByNumber(blockNumber int64) (core.Block, err block_parenthash, block_size, uncle_hash, - is_final + is_final, + block_miner, + block_extra_data, + block_reward, + block_uncles_reward FROM blocks WHERE node_id = $1 AND block_number = $2`, repository.nodeId, blockNumber) savedBlock, err := repository.loadBlock(blockRows) @@ -256,10 +260,10 @@ func (repository Postgres) insertBlock(block core.Block) error { tx, _ := repository.Db.BeginTx(context.Background(), nil) err := tx.QueryRow( `INSERT INTO blocks - (node_id, block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash, is_final) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + (node_id, block_number, block_gaslimit, block_gasused, block_time, block_difficulty, block_hash, block_nonce, block_parenthash, block_size, uncle_hash, is_final, block_miner, block_extra_data, block_reward, block_uncles_reward) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id `, - repository.nodeId, block.Number, block.GasLimit, block.GasUsed, block.Time, block.Difficulty, block.Hash, block.Nonce, block.ParentHash, block.Size, block.UncleHash, block.IsFinal). + repository.nodeId, block.Number, block.GasLimit, block.GasUsed, block.Time, block.Difficulty, block.Hash, block.Nonce, block.ParentHash, block.Size, block.UncleHash, block.IsFinal, block.Miner, block.ExtraData, block.Reward, block.UnclesReward). Scan(&blockId) if err != nil { tx.Rollback() @@ -305,15 +309,19 @@ func (repository Postgres) loadBlock(blockRows *sql.Row) (core.Block, error) { var blockHash string var blockNonce string var blockNumber int64 + var blockMiner string + var blockExtraData string var blockParentHash string var blockSize int64 var blockTime float64 + var blockReward float64 var difficulty int64 var gasLimit float64 var gasUsed float64 var uncleHash string + var unclesReward float64 var isFinal bool - err := blockRows.Scan(&blockId, &blockNumber, &gasLimit, &gasUsed, &blockTime, &difficulty, &blockHash, &blockNonce, &blockParentHash, &blockSize, &uncleHash, &isFinal) + err := blockRows.Scan(&blockId, &blockNumber, &gasLimit, &gasUsed, &blockTime, &difficulty, &blockHash, &blockNonce, &blockParentHash, &blockSize, &uncleHash, &isFinal, &blockMiner, &blockExtraData, &blockReward, &unclesReward) if err != nil { return core.Block{}, err } @@ -330,10 +338,14 @@ func (repository Postgres) loadBlock(blockRows *sql.Row) (core.Block, error) { ORDER BY tx_hash`, blockId) transactions := repository.loadTransactions(transactionRows) return core.Block{ + Reward: blockReward, Difficulty: difficulty, + ExtraData: blockExtraData, GasLimit: int64(gasLimit), GasUsed: int64(gasUsed), Hash: blockHash, + IsFinal: isFinal, + Miner: blockMiner, Nonce: blockNonce, Number: blockNumber, ParentHash: blockParentHash, @@ -341,7 +353,7 @@ func (repository Postgres) loadBlock(blockRows *sql.Row) (core.Block, error) { Time: int64(blockTime), Transactions: transactions, UncleHash: uncleHash, - IsFinal: isFinal, + UnclesReward: unclesReward, }, nil } diff --git a/pkg/repositories/testing/helpers.go b/pkg/repositories/testing/helpers.go index 70a71d18..ca7420db 100644 --- a/pkg/repositories/testing/helpers.go +++ b/pkg/repositories/testing/helpers.go @@ -61,37 +61,49 @@ func AssertRepositoryBehavior(buildRepository func(node core.Node) repositories. blockHash := "x123" blockParentHash := "x456" blockNonce := "0x881db2ca900682e9a9" + miner := "x123" + extraData := "xextraData" blockTime := int64(1508981640) uncleHash := "x789" blockSize := int64(1000) difficulty := int64(10) + blockReward := float64(5.132) + unclesReward := float64(3.580) block := core.Block{ - Difficulty: difficulty, - GasLimit: gasLimit, - GasUsed: gasUsed, - Hash: blockHash, - Nonce: blockNonce, - Number: blockNumber, - ParentHash: blockParentHash, - Size: blockSize, - Time: blockTime, - UncleHash: uncleHash, + Reward: blockReward, + Difficulty: difficulty, + GasLimit: gasLimit, + GasUsed: gasUsed, + Hash: blockHash, + ExtraData: extraData, + Nonce: blockNonce, + Miner: miner, + Number: blockNumber, + ParentHash: blockParentHash, + Size: blockSize, + Time: blockTime, + UncleHash: uncleHash, + UnclesReward: unclesReward, } repository.CreateOrUpdateBlock(block) savedBlock, err := repository.FindBlockByNumber(blockNumber) Expect(err).NotTo(HaveOccurred()) + Expect(savedBlock.Reward).To(Equal(blockReward)) Expect(savedBlock.Difficulty).To(Equal(difficulty)) Expect(savedBlock.GasLimit).To(Equal(gasLimit)) Expect(savedBlock.GasUsed).To(Equal(gasUsed)) Expect(savedBlock.Hash).To(Equal(blockHash)) Expect(savedBlock.Nonce).To(Equal(blockNonce)) + Expect(savedBlock.Miner).To(Equal(miner)) + Expect(savedBlock.ExtraData).To(Equal(extraData)) Expect(savedBlock.Number).To(Equal(blockNumber)) Expect(savedBlock.ParentHash).To(Equal(blockParentHash)) Expect(savedBlock.Size).To(Equal(blockSize)) Expect(savedBlock.Time).To(Equal(blockTime)) Expect(savedBlock.UncleHash).To(Equal(uncleHash)) + Expect(savedBlock.UnclesReward).To(Equal(unclesReward)) }) It("does not find a block when searching for a number that does not exist", func() {