github.com/NebulousLabs/Sia@v1.3.7/modules/consensus/synchronize_ibd_test.go (about)

     1  package consensus
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"path/filepath"
     9  	"strconv"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/NebulousLabs/Sia/build"
    15  	"github.com/NebulousLabs/Sia/modules"
    16  	"github.com/NebulousLabs/Sia/modules/gateway"
    17  	"github.com/NebulousLabs/Sia/types"
    18  )
    19  
    20  // TestSimpleInitialBlockchainDownload tests that
    21  // threadedInitialBlockchainDownload synchronizes with peers in the simple case
    22  // where there are 8 outbound peers with the same blockchain.
    23  func TestSimpleInitialBlockchainDownload(t *testing.T) {
    24  	if testing.Short() || !build.VLONG {
    25  		t.SkipNow()
    26  	}
    27  
    28  	// Create 8 remote peers.
    29  	remoteCSTs := make([]*consensusSetTester, 8)
    30  	for i := range remoteCSTs {
    31  		cst, err := blankConsensusSetTester(t.Name()+strconv.Itoa(i), modules.ProdDependencies)
    32  		if err != nil {
    33  			t.Fatal(err)
    34  		}
    35  		defer cst.Close()
    36  		remoteCSTs[i] = cst
    37  	}
    38  	// Create the "local" peer.
    39  	localCST, err := blankConsensusSetTester(t.Name()+"- local", modules.ProdDependencies)
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	}
    43  	defer localCST.Close()
    44  	for _, cst := range remoteCSTs {
    45  		err = localCST.cs.gateway.Connect(cst.cs.gateway.Address())
    46  		if err != nil {
    47  			t.Fatal(err)
    48  		}
    49  	}
    50  	// Give the OnConnectRPCs time to finish.
    51  	time.Sleep(5 * time.Second)
    52  
    53  	// Test IBD when all peers have only the genesis block.
    54  	doneChan := make(chan struct{})
    55  	go func() {
    56  		localCST.cs.threadedInitialBlockchainDownload()
    57  		doneChan <- struct{}{}
    58  	}()
    59  	select {
    60  	case <-doneChan:
    61  	case <-time.After(5 * time.Second):
    62  		t.Fatal("initialBlockchainDownload never completed")
    63  	}
    64  	if localCST.cs.CurrentBlock().ID() != remoteCSTs[0].cs.CurrentBlock().ID() {
    65  		t.Fatalf("current block ids do not match: expected '%v', got '%v'", remoteCSTs[0].cs.CurrentBlock().ID(), localCST.cs.CurrentBlock().ID())
    66  	}
    67  
    68  	// Test IBD when all remote peers have the same longest chain.
    69  	for i := 0; i < 20; i++ {
    70  		b, err := remoteCSTs[0].miner.FindBlock()
    71  		if err != nil {
    72  			t.Fatal(err)
    73  		}
    74  		for _, cst := range remoteCSTs {
    75  			_, err = cst.cs.managedAcceptBlocks([]types.Block{b})
    76  			if err != nil && err != modules.ErrBlockKnown && err != modules.ErrNonExtendingBlock {
    77  				t.Fatal(err)
    78  			}
    79  		}
    80  	}
    81  	go func() {
    82  		localCST.cs.threadedInitialBlockchainDownload()
    83  		doneChan <- struct{}{}
    84  	}()
    85  	select {
    86  	case <-doneChan:
    87  	case <-time.After(5 * time.Second):
    88  		t.Fatal("initialBlockchainDownload never completed")
    89  	}
    90  	if localCST.cs.CurrentBlock().ID() != remoteCSTs[0].cs.CurrentBlock().ID() {
    91  		t.Fatalf("current block ids do not match: expected '%v', got '%v'", remoteCSTs[0].cs.CurrentBlock().ID(), localCST.cs.CurrentBlock().ID())
    92  	}
    93  
    94  	// Test IBD when not starting from the genesis block.
    95  	for i := 0; i < 4; i++ {
    96  		b, err := remoteCSTs[0].miner.FindBlock()
    97  		if err != nil {
    98  			t.Fatal(err)
    99  		}
   100  		for _, cst := range remoteCSTs {
   101  			_, err = cst.cs.managedAcceptBlocks([]types.Block{b})
   102  			if err != nil && err != modules.ErrBlockKnown {
   103  				t.Fatal(err)
   104  			}
   105  		}
   106  	}
   107  	go func() {
   108  		localCST.cs.threadedInitialBlockchainDownload()
   109  		doneChan <- struct{}{}
   110  	}()
   111  	select {
   112  	case <-doneChan:
   113  	case <-time.After(5 * time.Second):
   114  		t.Fatal("initialBlockchainDownload never completed")
   115  	}
   116  	if localCST.cs.CurrentBlock().ID() != remoteCSTs[0].cs.CurrentBlock().ID() {
   117  		t.Fatalf("current block ids do not match: expected '%v', got '%v'", remoteCSTs[0].cs.CurrentBlock().ID(), localCST.cs.CurrentBlock().ID())
   118  	}
   119  
   120  	// Test IBD when the remote peers are on a longer fork.
   121  	for i := 0; i < 5; i++ {
   122  		b, err := localCST.miner.FindBlock()
   123  		if err != nil {
   124  			t.Fatal(err)
   125  		}
   126  		_, err = localCST.cs.managedAcceptBlocks([]types.Block{b})
   127  		if err != nil {
   128  			t.Fatal(err)
   129  		}
   130  	}
   131  	for i := 0; i < 10; i++ {
   132  		b, err := remoteCSTs[0].miner.FindBlock()
   133  		if err != nil {
   134  			t.Fatal(err)
   135  		}
   136  		for _, cst := range remoteCSTs {
   137  			_, err = cst.cs.managedAcceptBlocks([]types.Block{b})
   138  			if err != nil && err != modules.ErrBlockKnown {
   139  				t.Log(i)
   140  				t.Fatal(err)
   141  			}
   142  		}
   143  	}
   144  	go func() {
   145  		localCST.cs.threadedInitialBlockchainDownload()
   146  		doneChan <- struct{}{}
   147  	}()
   148  	select {
   149  	case <-doneChan:
   150  	case <-time.After(5 * time.Second):
   151  		t.Fatal("initialBlockchainDownload never completed")
   152  	}
   153  	if localCST.cs.CurrentBlock().ID() != remoteCSTs[0].cs.CurrentBlock().ID() {
   154  		t.Fatalf("current block ids do not match: expected '%v', got '%v'", remoteCSTs[0].cs.CurrentBlock().ID(), localCST.cs.CurrentBlock().ID())
   155  	}
   156  
   157  	// Test IBD when the remote peers are on a shorter fork.
   158  	for i := 0; i < 10; i++ {
   159  		b, err := localCST.miner.FindBlock()
   160  		if err != nil {
   161  			t.Fatal(err)
   162  		}
   163  		_, err = localCST.cs.managedAcceptBlocks([]types.Block{b})
   164  		if err != nil {
   165  			t.Fatal(err)
   166  		}
   167  	}
   168  	for i := 0; i < 5; i++ {
   169  		b, err := remoteCSTs[0].miner.FindBlock()
   170  		if err != nil {
   171  			t.Fatal(err)
   172  		}
   173  		for _, cst := range remoteCSTs {
   174  			_, err = cst.cs.managedAcceptBlocks([]types.Block{b})
   175  			if err != nil && err != modules.ErrBlockKnown {
   176  				t.Log(i)
   177  				t.Fatal(err)
   178  			}
   179  		}
   180  	}
   181  	localCurrentBlock := localCST.cs.CurrentBlock()
   182  	go func() {
   183  		localCST.cs.threadedInitialBlockchainDownload()
   184  		doneChan <- struct{}{}
   185  	}()
   186  	select {
   187  	case <-doneChan:
   188  	case <-time.After(5 * time.Second):
   189  		t.Fatal("initialBlockchainDownload never completed")
   190  	}
   191  	if localCST.cs.CurrentBlock().ID() != localCurrentBlock.ID() {
   192  		t.Fatalf("local was on a longer fork and should not have reorged")
   193  	}
   194  	if localCST.cs.CurrentBlock().ID() == remoteCSTs[0].cs.CurrentBlock().ID() {
   195  		t.Fatalf("ibd syncing is one way, and a longer fork on the local cs should not cause a reorg on the remote cs's")
   196  	}
   197  }
   198  
   199  type mockGatewayRPCError struct {
   200  	modules.Gateway
   201  	rpcErrs map[modules.NetAddress]error
   202  	mu      sync.Mutex
   203  }
   204  
   205  func (g *mockGatewayRPCError) RPC(addr modules.NetAddress, name string, fn modules.RPCFunc) error {
   206  	g.mu.Lock()
   207  	defer g.mu.Unlock()
   208  	return g.rpcErrs[addr]
   209  }
   210  
   211  // TestInitialBlockChainDownloadDisconnects tests that
   212  // threadedInitialBlockchainDownload only disconnects from peers that error
   213  // with anything but a timeout.
   214  func TestInitialBlockchainDownloadDisconnects(t *testing.T) {
   215  	if testing.Short() {
   216  		t.SkipNow()
   217  	}
   218  
   219  	testdir := build.TempDir(modules.ConsensusDir, t.Name())
   220  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, "local", modules.GatewayDir))
   221  	if err != nil {
   222  		t.Fatal(err)
   223  	}
   224  	defer g.Close()
   225  	mg := mockGatewayRPCError{
   226  		Gateway: g,
   227  		rpcErrs: make(map[modules.NetAddress]error),
   228  	}
   229  	localCS, err := New(&mg, false, filepath.Join(testdir, "local", modules.ConsensusDir))
   230  	if err != nil {
   231  		t.Fatal(err)
   232  	}
   233  	defer localCS.Close()
   234  
   235  	rpcErrs := []error{
   236  		// rpcErrs that should cause a a disconnect.
   237  		io.EOF,
   238  		errors.New("random error"),
   239  		errSendBlocksStalled,
   240  		// rpcErrs that should not cause a disconnect.
   241  		mockNetError{
   242  			error:   errors.New("Read timeout"),
   243  			timeout: true,
   244  		},
   245  		// Need at least minNumOutbound peers that return nil for
   246  		// threadedInitialBlockchainDownload to mark IBD done.
   247  		nil, nil, nil, nil, nil,
   248  	}
   249  	for i, rpcErr := range rpcErrs {
   250  		g, err := gateway.New("localhost:0", false, filepath.Join(testdir, "remote - "+strconv.Itoa(i), modules.GatewayDir))
   251  		if err != nil {
   252  			t.Fatal(err)
   253  		}
   254  		defer g.Close()
   255  		err = localCS.gateway.Connect(g.Address())
   256  		if err != nil {
   257  			t.Fatal(err)
   258  		}
   259  		mg.rpcErrs[g.Address()] = rpcErr
   260  	}
   261  	// Sleep to to give the OnConnectRPCs time to finish.
   262  	time.Sleep(500 * time.Millisecond)
   263  	// Do IBD.
   264  	localCS.threadedInitialBlockchainDownload()
   265  	// Check that localCS disconnected from peers that errored but did not time out during SendBlocks.
   266  	if len(localCS.gateway.Peers()) != 6 {
   267  		t.Error("threadedInitialBlockchainDownload disconnected from peers that timedout or didn't error", len(localCS.gateway.Peers()))
   268  	}
   269  	for _, p := range localCS.gateway.Peers() {
   270  		err = mg.rpcErrs[p.NetAddress]
   271  		if err == nil {
   272  			continue
   273  		}
   274  		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
   275  			continue
   276  		}
   277  		t.Fatalf("threadedInitialBlockchainDownload didn't disconnect from a peer that returned '%v', %v", err, p.NetAddress)
   278  	}
   279  }
   280  
   281  // TestInitialBlockchainDownloadDoneRules tests that
   282  // threadedInitialBlockchainDownload only terminates under the appropriate
   283  // conditions. Appropriate conditions are:
   284  //  - at least minNumOutbound synced outbound peers
   285  //  - or at least 1 synced outbound peer and minIBDWaitTime has passed since beginning IBD.
   286  func TestInitialBlockchainDownloadDoneRules(t *testing.T) {
   287  	if testing.Short() || !build.VLONG {
   288  		t.SkipNow()
   289  	}
   290  	testdir := build.TempDir(modules.ConsensusDir, t.Name())
   291  
   292  	// Create a gateway that can be forced to return errors when its RPC method
   293  	// is called, then create a consensus set using that gateway.
   294  	g, err := gateway.New("localhost:0", false, filepath.Join(testdir, "local", modules.GatewayDir))
   295  	if err != nil {
   296  		t.Fatal(err)
   297  	}
   298  	defer g.Close()
   299  	mg := mockGatewayRPCError{
   300  		Gateway: g,
   301  		rpcErrs: make(map[modules.NetAddress]error),
   302  	}
   303  	cs, err := New(&mg, false, filepath.Join(testdir, "local", modules.ConsensusDir))
   304  	if err != nil {
   305  		t.Fatal(err)
   306  	}
   307  	defer cs.Close()
   308  
   309  	// Verify that the consensus set will not signal IBD completion when it has
   310  	// zero peers.
   311  	doneChan := make(chan struct{})
   312  	go func() {
   313  		cs.threadedInitialBlockchainDownload()
   314  		doneChan <- struct{}{}
   315  	}()
   316  	select {
   317  	case <-doneChan:
   318  		t.Error("threadedInitialBlockchainDownload finished with 0 synced peers")
   319  	case <-time.After(minIBDWaitTime + ibdLoopDelay):
   320  	}
   321  
   322  	// threadedInitialBlockchainDownload is already running. Feed some inbound
   323  	// peers to the consensus set. The gateway, through its own process of
   324  	// trying to find outbound peers, will eventually convert one of the
   325  	// inbound peers to an outbound peer. IBD should not complete until there
   326  	// is at least one outbound peer.
   327  	//
   328  	// After this function has completed, all of the peers will be shutdown,
   329  	// leaving the consensus set once again with zero peers.
   330  	func() {
   331  		inboundCSTs := make([]*consensusSetTester, 8)
   332  		for i := 0; i < len(inboundCSTs); i++ {
   333  			inboundCST, err := blankConsensusSetTester(filepath.Join(t.Name(), " - inbound "+strconv.Itoa(i)), modules.ProdDependencies)
   334  			if err != nil {
   335  				t.Fatal(err)
   336  			}
   337  			defer inboundCST.Close()
   338  			inboundCST.cs.gateway.Connect(cs.gateway.Address())
   339  		}
   340  		<-doneChan
   341  		peers := cs.gateway.Peers()
   342  		outbound := false
   343  		for _, p := range peers {
   344  			if !p.Inbound {
   345  				outbound = true
   346  				break
   347  			}
   348  		}
   349  		if !outbound {
   350  			t.Error("threadedInitialBlockchainDownload finished with only inbound peers")
   351  		}
   352  	}()
   353  
   354  	// Try another initial blockchain download, this time with an outbound peer
   355  	// who is not synced. The consensus set should not determine itself to have
   356  	// completed IBD with only unsynced peers.
   357  	//
   358  	// 'NotSynced' is simulated in this peer by having all RPCs return errors.
   359  	go func() {
   360  		cs.threadedInitialBlockchainDownload()
   361  		doneChan <- struct{}{}
   362  	}()
   363  	gatewayTimesout, err := gateway.New("localhost:0", false, filepath.Join(testdir, "remote - timesout", modules.GatewayDir))
   364  	if err != nil {
   365  		t.Fatal(err)
   366  	}
   367  	defer gatewayTimesout.Close()
   368  	mg.mu.Lock()
   369  	mg.rpcErrs[gatewayTimesout.Address()] = mockNetError{
   370  		error:   errors.New("Read timeout"),
   371  		timeout: true,
   372  	}
   373  	mg.mu.Unlock()
   374  	err = cs.gateway.Connect(gatewayTimesout.Address())
   375  	if err != nil {
   376  		t.Fatal(err)
   377  	}
   378  	select {
   379  	case <-doneChan:
   380  		t.Error("threadedInitialBlockchainDownload finished with 0 synced peers")
   381  	case <-time.After(minIBDWaitTime + ibdLoopDelay):
   382  	}
   383  
   384  	// Add a peer that is synced to the peer that is not synced. IBD should not
   385  	// be considered completed when there is a tie between synced and
   386  	// not-synced peers.
   387  	gatewayNoTimeout, err := gateway.New("localhost:0", false, filepath.Join(testdir, "remote - no timeout1", modules.GatewayDir))
   388  	if err != nil {
   389  		t.Fatal(err)
   390  	}
   391  	defer gatewayNoTimeout.Close()
   392  	mg.mu.Lock()
   393  	mg.rpcErrs[gatewayNoTimeout.Address()] = nil
   394  	mg.mu.Unlock()
   395  	err = cs.gateway.Connect(gatewayNoTimeout.Address())
   396  	if err != nil {
   397  		t.Fatal(err)
   398  	}
   399  	select {
   400  	case <-doneChan:
   401  		t.Fatal("threadedInitialBlockchainDownload finished with 1 synced peer and 1 non-synced peer")
   402  	case <-time.After(minIBDWaitTime + ibdLoopDelay):
   403  	}
   404  
   405  	// Test when there is 2 peers that are synced and one that is not synced.
   406  	// There is now a majority synced peers and the minIBDWaitTime has passed,
   407  	// so the IBD function should finish.
   408  	gatewayNoTimeout2, err := gateway.New("localhost:0", false, filepath.Join(testdir, "remote - no timeout2", modules.GatewayDir))
   409  	if err != nil {
   410  		t.Fatal(err)
   411  	}
   412  	defer gatewayNoTimeout2.Close()
   413  	mg.mu.Lock()
   414  	mg.rpcErrs[gatewayNoTimeout2.Address()] = nil
   415  	mg.mu.Unlock()
   416  	err = cs.gateway.Connect(gatewayNoTimeout2.Address())
   417  	if err != nil {
   418  		t.Fatal(err)
   419  	}
   420  	select {
   421  	case <-doneChan:
   422  	case <-time.After(4 * (minIBDWaitTime + ibdLoopDelay)):
   423  		t.Fatal("threadedInitialBlockchainDownload never finished with 2 synced peers and 1 non-synced peer")
   424  	}
   425  
   426  	// Test when there are >= minNumOutbound peers and >= minNumOutbound peers are synced.
   427  	gatewayNoTimeouts := make([]modules.Gateway, minNumOutbound-1)
   428  	for i := 0; i < len(gatewayNoTimeouts); i++ {
   429  		tmpG, err := gateway.New("localhost:0", false, filepath.Join(testdir, fmt.Sprintf("remote - no timeout-auto-%v", i+3), modules.GatewayDir))
   430  		if err != nil {
   431  			t.Fatal(err)
   432  		}
   433  		defer tmpG.Close()
   434  		mg.mu.Lock()
   435  		mg.rpcErrs[tmpG.Address()] = nil
   436  		mg.mu.Unlock()
   437  		gatewayNoTimeouts[i] = tmpG
   438  		err = cs.gateway.Connect(gatewayNoTimeouts[i].Address())
   439  		if err != nil {
   440  			t.Fatal(err)
   441  		}
   442  	}
   443  	go func() {
   444  		cs.threadedInitialBlockchainDownload()
   445  		doneChan <- struct{}{}
   446  	}()
   447  	select {
   448  	case <-doneChan:
   449  	case <-time.After(minIBDWaitTime):
   450  		t.Fatal("threadedInitialBlockchainDownload didn't finish in less than minIBDWaitTime")
   451  	}
   452  }
   453  
   454  // TestGenesisBlockSync is a regression test that checks what happens when two
   455  // consensus sets with only the genesis block are connected. They should
   456  // determine that they are sync'd, however previously they would not sync to
   457  // eachother as they would report EOF instead of performing correct block
   458  // exchange.
   459  func TestGenesisBlockSync(t *testing.T) {
   460  	if testing.Short() || !build.VLONG {
   461  		t.SkipNow()
   462  	}
   463  
   464  	// Create two consensus sets that have zero blocks each (except for the
   465  	// genesis block).
   466  	cst1, err := blankConsensusSetTester(t.Name()+"1", modules.ProdDependencies)
   467  	if err != nil {
   468  		t.Fatal(err)
   469  	}
   470  	cst2, err := blankConsensusSetTester(t.Name()+"2", modules.ProdDependencies)
   471  	if err != nil {
   472  		t.Fatal(err)
   473  	}
   474  
   475  	// Connect them.
   476  	err = cst1.gateway.Connect(cst2.gateway.Address())
   477  	if err != nil {
   478  		t.Fatal(err)
   479  	}
   480  	// Block until both report that they are sync'd.
   481  	for i := 0; i < 100; i++ {
   482  		time.Sleep(time.Millisecond * 100)
   483  		if cst1.cs.Synced() && cst2.cs.Synced() {
   484  			break
   485  		}
   486  	}
   487  	if !cst1.cs.Synced() || !cst2.cs.Synced() {
   488  		t.Error("Consensus sets did not synchronize to eachother", cst1.cs.Synced(), cst2.cs.Synced())
   489  	}
   490  
   491  	time.Sleep(time.Second * 12)
   492  	if len(cst1.gateway.Peers()) == 0 {
   493  		t.Error("disconnection occurred!")
   494  	}
   495  }