From 421951b4e0467fc6e11d93661c67da0348418893 Mon Sep 17 00:00:00 2001 From: Lukas Lukac Date: Sun, 6 Jun 2021 11:49:55 +0200 Subject: [PATCH] Makes tests faster by using smaller mining difficulty. --- cmd/tbb/balances.go | 6 ++++-- cmd/tbb/run.go | 2 +- database/block.go | 20 ++++++++++++++----- database/state.go | 21 ++++++++++++++------ node/miner.go | 9 +++++---- node/miner_test.go | 23 ++++++++++++---------- node/node.go | 36 ++++++++++++++++++++++------------- node/node_integration_test.go | 28 ++++++++++++--------------- 8 files changed, 88 insertions(+), 57 deletions(-) diff --git a/cmd/tbb/balances.go b/cmd/tbb/balances.go index c75a28e..bfb4bc3 100644 --- a/cmd/tbb/balances.go +++ b/cmd/tbb/balances.go @@ -17,9 +17,11 @@ package main import ( "fmt" + "os" + "github.com/spf13/cobra" "github.com/web3coach/the-blockchain-bar/database" - "os" + "github.com/web3coach/the-blockchain-bar/node" ) func balancesCmd() *cobra.Command { @@ -43,7 +45,7 @@ func balancesListCmd() *cobra.Command { Use: "list", Short: "Lists all balances.", Run: func(cmd *cobra.Command, args []string) { - state, err := database.NewStateFromDisk(getDataDirFromCmd(cmd)) + state, err := database.NewStateFromDisk(getDataDirFromCmd(cmd), node.DefaultMiningDifficulty) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) diff --git a/cmd/tbb/run.go b/cmd/tbb/run.go index 6894a53..4ba3b2a 100644 --- a/cmd/tbb/run.go +++ b/cmd/tbb/run.go @@ -55,7 +55,7 @@ func runCmd() *cobra.Command { } version := fmt.Sprintf("%s.%s.%s-alpha %s %s", Major, Minor, Fix, shortGitCommit(GitCommit), Verbal) - n := node.New(getDataDirFromCmd(cmd), ip, port, database.NewAccount(miner), bootstrap, version) + n := node.New(getDataDirFromCmd(cmd), ip, port, database.NewAccount(miner), bootstrap, version, node.DefaultMiningDifficulty) err := n.Run(context.Background(), isSSLDisabled, sslEmail) if err != nil { fmt.Println(err) diff --git a/database/block.go b/database/block.go index bdb1694..8e45b80 100644 --- a/database/block.go +++ b/database/block.go @@ -21,6 +21,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/ethereum/go-ethereum/common" ) @@ -78,9 +79,18 @@ func (b Block) Hash() (Hash, error) { return sha256.Sum256(blockJson), nil } -func IsBlockHashValid(hash Hash) bool { - return fmt.Sprintf("%x", hash[0]) == "0" && - fmt.Sprintf("%x", hash[1]) == "0" && - fmt.Sprintf("%x", hash[2]) == "0" && - fmt.Sprintf("%x", hash[3]) != "0" +func IsBlockHashValid(hash Hash, miningDifficulty uint) bool { + zeroesCount := uint(0) + + for i := uint(0); i < miningDifficulty; i++ { + if fmt.Sprintf("%x", hash[i]) == "0" { + zeroesCount++ + } + } + + if fmt.Sprintf("%x", hash[miningDifficulty]) == "0" { + return false + } + + return zeroesCount == miningDifficulty } diff --git a/database/state.go b/database/state.go index 820c417..07df252 100644 --- a/database/state.go +++ b/database/state.go @@ -19,10 +19,11 @@ import ( "bufio" "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/common" "os" "reflect" "sort" + + "github.com/ethereum/go-ethereum/common" ) const TxFee = uint(50) @@ -36,9 +37,11 @@ type State struct { latestBlock Block latestBlockHash Hash hasGenesisBlock bool + + miningDifficulty uint } -func NewStateFromDisk(dataDir string) (*State, error) { +func NewStateFromDisk(dataDir string, miningDifficulty uint) (*State, error) { err := InitDataDirIfNotExists(dataDir, []byte(genesisJson)) if err != nil { return nil, err @@ -64,7 +67,7 @@ func NewStateFromDisk(dataDir string) (*State, error) { scanner := bufio.NewScanner(f) - state := &State{balances, account2nonce, f, Block{}, Hash{}, false} + state := &State{balances, account2nonce, f, Block{}, Hash{}, false, miningDifficulty} for scanner.Scan() { if err := scanner.Err(); err != nil { @@ -140,6 +143,7 @@ func (s *State) AddBlock(b Block) (Hash, error) { s.latestBlockHash = blockHash s.latestBlock = b s.hasGenesisBlock = true + s.miningDifficulty = pendingState.miningDifficulty return blockHash, nil } @@ -164,8 +168,8 @@ func (s *State) GetNextAccountNonce(account common.Address) uint { return s.Account2Nonce[account] + 1 } -func (s *State) Close() error { - return s.dbFile.Close() +func (c *State) ChangeMiningDifficulty(newDifficulty uint) { + c.miningDifficulty = newDifficulty } func (s *State) Copy() State { @@ -175,6 +179,7 @@ func (s *State) Copy() State { c.latestBlockHash = s.latestBlockHash c.Balances = make(map[common.Address]uint) c.Account2Nonce = make(map[common.Address]uint) + c.miningDifficulty = s.miningDifficulty for acc, balance := range s.Balances { c.Balances[acc] = balance @@ -187,6 +192,10 @@ func (s *State) Copy() State { return c } +func (s *State) Close() error { + return s.dbFile.Close() +} + // applyBlock verifies if block can be added to the blockchain. // // Block metadata are verified as well as transactions within (sufficient balances, etc). @@ -206,7 +215,7 @@ func applyBlock(b Block, s *State) error { return err } - if !IsBlockHashValid(hash) { + if !IsBlockHashValid(hash, s.miningDifficulty) { return fmt.Errorf("invalid block hash %x", hash) } diff --git a/node/miner.go b/node/miner.go index f87c574..5f18e8a 100644 --- a/node/miner.go +++ b/node/miner.go @@ -18,11 +18,12 @@ package node import ( "context" "fmt" + "math/rand" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/web3coach/the-blockchain-bar/database" "github.com/web3coach/the-blockchain-bar/fs" - "math/rand" - "time" ) type PendingBlock struct { @@ -37,7 +38,7 @@ func NewPendingBlock(parent database.Hash, number uint64, miner common.Address, return PendingBlock{parent, number, uint64(time.Now().Unix()), miner, txs} } -func Mine(ctx context.Context, pb PendingBlock) (database.Block, error) { +func Mine(ctx context.Context, pb PendingBlock, miningDifficulty uint) (database.Block, error) { if len(pb.txs) == 0 { return database.Block{}, fmt.Errorf("mining empty blocks is not allowed") } @@ -48,7 +49,7 @@ func Mine(ctx context.Context, pb PendingBlock) (database.Block, error) { var hash database.Hash var nonce uint32 - for !database.IsBlockHashValid(hash) { + for !database.IsBlockHashValid(hash, miningDifficulty) { select { case <-ctx.Done(): fmt.Println("Mining cancelled!") diff --git a/node/miner_test.go b/node/miner_test.go index a480b5c..1ef72a5 100644 --- a/node/miner_test.go +++ b/node/miner_test.go @@ -21,33 +21,36 @@ import ( "crypto/elliptic" "crypto/rand" "encoding/hex" + "testing" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/web3coach/the-blockchain-bar/database" "github.com/web3coach/the-blockchain-bar/wallet" - "testing" - "time" ) +const defaultTestMiningDifficulty = 2 + func TestValidBlockHash(t *testing.T) { - hexHash := "000000fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" + hexHash := "0000fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" var hash = database.Hash{} hex.Decode(hash[:], []byte(hexHash)) - isValid := database.IsBlockHashValid(hash) + isValid := database.IsBlockHashValid(hash, defaultTestMiningDifficulty) if !isValid { - t.Fatalf("hash '%s' starting with 6 zeroes is suppose to be valid", hexHash) + t.Fatalf("hash '%s' starting with 4 zeroes is suppose to be valid", hexHash) } } func TestInvalidBlockHash(t *testing.T) { - hexHash := "000001fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" + hexHash := "0001fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" var hash = database.Hash{} hex.Decode(hash[:], []byte(hexHash)) - isValid := database.IsBlockHashValid(hash) + isValid := database.IsBlockHashValid(hash, defaultTestMiningDifficulty) if isValid { t.Fatal("hash is not suppose to be valid") } @@ -66,7 +69,7 @@ func TestMine(t *testing.T) { ctx := context.Background() - minedBlock, err := Mine(ctx, pendingBlock) + minedBlock, err := Mine(ctx, pendingBlock, defaultTestMiningDifficulty) if err != nil { t.Fatal(err) } @@ -76,7 +79,7 @@ func TestMine(t *testing.T) { t.Fatal(err) } - if !database.IsBlockHashValid(minedBlockHash) { + if !database.IsBlockHashValid(minedBlockHash, defaultTestMiningDifficulty) { t.Fatal() } @@ -98,7 +101,7 @@ func TestMineWithTimeout(t *testing.T) { ctx, _ := context.WithTimeout(context.Background(), time.Microsecond*100) - _, err = Mine(ctx, pendingBlock) + _, err = Mine(ctx, pendingBlock, defaultTestMiningDifficulty) if err == nil { t.Fatal(err) } diff --git a/node/node.go b/node/node.go index fbe8394..105f648 100644 --- a/node/node.go +++ b/node/node.go @@ -47,6 +47,7 @@ const endpointAddPeerQueryKeyMiner = "miner" const endpointAddPeerQueryKeyVersion = "version" const miningIntervalSeconds = 10 +const DefaultMiningDifficulty = 3 type PeerNode struct { IP string `json:"ip"` @@ -86,23 +87,27 @@ type Node struct { archivedTXs map[string]database.SignedTx newSyncedBlocks chan database.Block newPendingTXs chan database.SignedTx - isMining bool nodeVersion string + + // Number of zeroes the hash must start with to be considered valid. Default 3 + miningDifficulty uint + isMining bool } -func New(dataDir string, ip string, port uint64, acc common.Address, bootstrap PeerNode, version string) *Node { +func New(dataDir string, ip string, port uint64, acc common.Address, bootstrap PeerNode, version string, miningDifficulty uint) *Node { knownPeers := make(map[string]PeerNode) n := &Node{ - dataDir: dataDir, - info: NewPeerNode(ip, port, false, acc, true, version), - knownPeers: knownPeers, - pendingTXs: make(map[string]database.SignedTx), - archivedTXs: make(map[string]database.SignedTx), - newSyncedBlocks: make(chan database.Block), - newPendingTXs: make(chan database.SignedTx, 10000), - isMining: false, - nodeVersion: version, + dataDir: dataDir, + info: NewPeerNode(ip, port, false, acc, true, version), + knownPeers: knownPeers, + pendingTXs: make(map[string]database.SignedTx), + archivedTXs: make(map[string]database.SignedTx), + newSyncedBlocks: make(chan database.Block), + newPendingTXs: make(chan database.SignedTx, 10000), + nodeVersion: version, + isMining: false, + miningDifficulty: miningDifficulty, } n.AddPeer(bootstrap) @@ -117,7 +122,7 @@ func NewPeerNode(ip string, port uint64, isBootstrap bool, acc common.Address, c func (n *Node) Run(ctx context.Context, isSSLDisabled bool, sslEmail string) error { fmt.Println(fmt.Sprintf("Listening on: %s:%d", n.info.IP, n.info.Port)) - state, err := database.NewStateFromDisk(n.dataDir) + state, err := database.NewStateFromDisk(n.dataDir, n.miningDifficulty) if err != nil { return err } @@ -234,7 +239,7 @@ func (n *Node) minePendingTXs(ctx context.Context) error { n.getPendingTXsAsArray(), ) - minedBlock, err := Mine(ctx, blockToMine) + minedBlock, err := Mine(ctx, blockToMine, n.miningDifficulty) if err != nil { return err } @@ -265,6 +270,11 @@ func (n *Node) removeMinedPendingTXs(block database.Block) { } } +func (n *Node) ChangeMiningDifficulty(newDifficulty uint) { + n.miningDifficulty = newDifficulty + n.state.ChangeMiningDifficulty(newDifficulty) +} + func (n *Node) AddPeer(peer PeerNode) { n.knownPeers[peer.TcpAddress()] = peer } diff --git a/node/node_integration_test.go b/node/node_integration_test.go index 6fdd87f..54d60b0 100644 --- a/node/node_integration_test.go +++ b/node/node_integration_test.go @@ -61,7 +61,7 @@ func TestNode_Run(t *testing.T) { t.Fatal(err) } - n := New(datadir, "127.0.0.1", 8085, database.NewAccount(DefaultMiner), PeerNode{}, nodeVersion) + n := New(datadir, "127.0.0.1", 8085, database.NewAccount(DefaultMiner), PeerNode{}, nodeVersion, defaultTestMiningDifficulty) ctx, _ := context.WithTimeout(context.Background(), time.Second*5) err = n.Run(ctx, true, "") @@ -90,7 +90,7 @@ func TestNode_Mining(t *testing.T) { // Construct a new Node instance and configure // Andrej as a miner - n := New(dataDir, nInfo.IP, nInfo.Port, andrej, nInfo, nodeVersion) + n := New(dataDir, nInfo.IP, nInfo.Port, andrej, nInfo, nodeVersion, defaultTestMiningDifficulty) // Allow the mining to run for 30 mins, in the worst case ctx, closeNode := context.WithTimeout( @@ -187,7 +187,7 @@ func TestNode_ForgedTx(t *testing.T) { } defer fs.RemoveDir(dataDir) - n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion) + n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) ctx, closeNode := context.WithTimeout(context.Background(), time.Minute*30) andrejPeerNode := NewPeerNode("127.0.0.1", 8085, false, andrej, true, nodeVersion) @@ -275,7 +275,7 @@ func TestNode_ReplayedTx(t *testing.T) { } defer fs.RemoveDir(dataDir) - n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion) + n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) ctx, closeNode := context.WithCancel(context.Background()) andrejPeerNode := NewPeerNode("127.0.0.1", 8085, false, andrej, true, nodeVersion) babaYagaPeerNode := NewPeerNode("127.0.0.1", 8086, false, babaYaga, true, nodeVersion) @@ -401,7 +401,8 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { nodeVersion, ) - n := New(dataDir, nInfo.IP, nInfo.Port, babaYaga, nInfo, nodeVersion) + // Start mining with a high mining difficulty, just to be slow on purpose and let a synced block arrive first + n := New(dataDir, nInfo.IP, nInfo.Port, babaYaga, nInfo, nodeVersion, uint(5)) // Allow the test to run for 30 mins, in the worst case ctx, closeNode := context.WithTimeout(context.Background(), time.Minute*30) @@ -430,7 +431,7 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { // with Andrej as a miner who will receive the block reward, // to simulate the block came on the fly from another peer validPreMinedPb := NewPendingBlock(database.Hash{}, 0, andrej, []database.SignedTx{signedTx1}) - validSyncedBlock, err := Mine(ctx, validPreMinedPb) + validSyncedBlock, err := Mine(ctx, validPreMinedPb, defaultTestMiningDifficulty) if err != nil { t.Fatal(err) } @@ -450,9 +451,6 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { } }() - // TODO: Fix a race condition when the block gets mined - // before the validBlock gets synced. - // // Interrupt the previously started mining with a new synced block // BUT this block contains only 1 TX the previous mining activity tried to mine // which means the mining will start again for the one pending TX that is left and wasn't in @@ -463,6 +461,9 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { t.Fatal("should be mining") } + // Change the mining difficulty back to the testing level from previously purposefully slow, high value + // otherwise the synced block would be invalid. + n.ChangeMiningDifficulty(defaultTestMiningDifficulty) _, err := n.state.AddBlock(validSyncedBlock) if err != nil { t.Fatal(err) @@ -470,7 +471,7 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { // Mock the Andrej's block came from a network n.newSyncedBlocks <- validSyncedBlock - time.Sleep(time.Second * 2) + time.Sleep(time.Second) if n.isMining { t.Fatal("synced block should have canceled mining") } @@ -481,11 +482,6 @@ func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { if len(n.pendingTXs) != 1 && !onlyTX2IsPending { t.Fatal("synced block should have canceled mining of already mined TX") } - - time.Sleep(time.Second * (miningIntervalSeconds + 2)) - if !n.isMining { - t.Fatal("should be mining again the 1 TX not included in synced block") - } }() go func() { @@ -564,7 +560,7 @@ func TestNode_MiningSpamTransactions(t *testing.T) { } defer fs.RemoveDir(dataDir) - n := New(dataDir, "127.0.0.1", 8085, miner, PeerNode{}, nodeVersion) + n := New(dataDir, "127.0.0.1", 8085, miner, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) ctx, closeNode := context.WithCancel(context.Background()) minerPeerNode := NewPeerNode("127.0.0.1", 8085, false, miner, true, nodeVersion)