gitlab.com/SiaPrime/SiaPrime@v1.4.1/node/api/ecosystem_helpers_test.go (about)

     1  package api
     2  
     3  // ecosystem_helpers_test.go has a bunch of helper functions to make setting up
     4  // large ecosystem tests easier.
     5  //
     6  // List of helper functions:
     7  //    addStorageToAllHosts // adds a storage folder to every host
     8  //    announceAllHosts     // announce all hosts to the network (and mine a block)
     9  //    fullyConnectNodes    // connects each server tester to all the others
    10  //    fundAllNodes         // mines blocks until all server testers have money
    11  //    synchronizationCheck // checks that all server testers have the same recent block
    12  //    waitForBlock         // block until the provided block is the most recent block for all server testers
    13  
    14  import (
    15  	"errors"
    16  	"fmt"
    17  	"net/url"
    18  	"time"
    19  
    20  	"gitlab.com/SiaPrime/SiaPrime/build"
    21  	"gitlab.com/SiaPrime/SiaPrime/types"
    22  )
    23  
    24  // addStorageToAllHosts adds a storage folder with a bunch of storage to each
    25  // host.
    26  func addStorageToAllHosts(sts []*serverTester) error {
    27  	for _, st := range sts {
    28  		values := url.Values{}
    29  		values.Set("path", st.dir)
    30  		values.Set("size", "1048576")
    31  		err := st.stdPostAPI("/host/storage/folders/add", values)
    32  		if err != nil {
    33  			return err
    34  		}
    35  	}
    36  	return nil
    37  }
    38  
    39  // announceAllHosts will announce every host in the tester set to the
    40  // blockchain.
    41  func announceAllHosts(sts []*serverTester) error {
    42  	// Check that all announcements will be on the same chain.
    43  	_, err := synchronizationCheck(sts)
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	// Grab the initial transaction pool size to know how many total transactions
    49  	// there should be after announcement.
    50  	initialTpoolSize := len(sts[0].tpool.TransactionList())
    51  
    52  	// Announce each host.
    53  	for _, st := range sts {
    54  		// Set the host to be accepting contracts.
    55  		acceptingContractsValues := url.Values{}
    56  		acceptingContractsValues.Set("acceptingcontracts", "true")
    57  		err = st.stdPostAPI("/host", acceptingContractsValues)
    58  		if err != nil {
    59  			return err
    60  		}
    61  
    62  		// Fetch the host net address.
    63  		var hg HostGET
    64  		err = st.getAPI("/host", &hg)
    65  		if err != nil {
    66  			return err
    67  		}
    68  
    69  		// Make the announcement.
    70  		announceValues := url.Values{}
    71  		announceValues.Set("address", string(hg.ExternalSettings.NetAddress))
    72  		err = st.stdPostAPI("/host/announce", announceValues)
    73  		if err != nil {
    74  			return err
    75  		}
    76  	}
    77  
    78  	// Wait until all of the transactions have propagated to all of the nodes.
    79  	//
    80  	// TODO: Replace this direct transaction pool call with a call to the
    81  	// /transactionpool endpoint.
    82  	//
    83  	// TODO: At some point the number of transactions needed to make an
    84  	// announcement may change. Currently its 2.
    85  	for i := 0; i < 50; i++ {
    86  		if len(sts[0].tpool.TransactionList()) == len(sts)*2+initialTpoolSize {
    87  			break
    88  		}
    89  		time.Sleep(time.Millisecond * 100)
    90  	}
    91  	if len(sts[0].tpool.TransactionList()) < len(sts)*2+initialTpoolSize {
    92  		return fmt.Errorf("Host announcements do not seem to have propagated to the leader's tpool: %v, %v, %v", len(sts), len(sts[0].tpool.TransactionList())+initialTpoolSize, initialTpoolSize)
    93  	}
    94  
    95  	// Mine a block and then wait for all of the nodes to syncrhonize to it.
    96  	_, err = sts[0].miner.AddBlock()
    97  	if err != nil {
    98  		return err
    99  	}
   100  	// Block until the block propagated to all nodes
   101  	for _, st := range sts[1:] {
   102  		err = waitForBlock(sts[0].cs.CurrentBlock().ID(), st)
   103  		if err != nil {
   104  			return (err)
   105  		}
   106  	}
   107  	// Check if all nodes are on the same block now
   108  	_, err = synchronizationCheck(sts)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// Block until every node has completed the scan of every other node, so
   114  	// that each node has a full hostdb.
   115  	for _, st := range sts {
   116  		err := build.Retry(600, 100*time.Millisecond, func() error {
   117  			var ah HostdbActiveGET
   118  			err = st.getAPI("/hostdb/active", &ah)
   119  			if err != nil {
   120  				return err
   121  			}
   122  			if len(ah.Hosts) < len(sts) {
   123  				return errors.New("one of the nodes hostdbs was unable to find at least one host announcement")
   124  			}
   125  			return nil
   126  		})
   127  		if err != nil {
   128  			return err
   129  		}
   130  	}
   131  	return nil
   132  }
   133  
   134  // fullyConnectNodes takes a bunch of tester nodes and connects each to the
   135  // other, creating a fully connected graph so that everyone is on the same
   136  // chain.
   137  //
   138  // After connecting the nodes, it verifies that all the nodes have
   139  // synchronized.
   140  func fullyConnectNodes(sts []*serverTester) error {
   141  	for i, sta := range sts {
   142  		var gg GatewayGET
   143  		err := sta.getAPI("/gateway", &gg)
   144  		if err != nil {
   145  			return err
   146  		}
   147  
   148  		// Connect this node to every other node.
   149  		for _, stb := range sts[i+1:] {
   150  			// Try connecting to the other node until both have the other in
   151  			// their peer list.
   152  			err = build.Retry(100, time.Millisecond*100, func() error {
   153  				// NOTE: this check depends on string-matching an error in the
   154  				// gateway. If that error changes at all, this string will need to
   155  				// be updated.
   156  				err := stb.stdPostAPI("/gateway/connect/"+string(gg.NetAddress), nil)
   157  				if err != nil && err.Error() != "already connected to this peer" {
   158  					return err
   159  				}
   160  
   161  				// Check that the gateways are connected.
   162  				bToA := false
   163  				aToB := false
   164  				var ggb GatewayGET
   165  				err = stb.getAPI("/gateway", &ggb)
   166  				if err != nil {
   167  					return err
   168  				}
   169  				for _, peer := range ggb.Peers {
   170  					if peer.NetAddress == gg.NetAddress {
   171  						bToA = true
   172  						break
   173  					}
   174  				}
   175  				err = sta.getAPI("/gateway", &gg)
   176  				if err != nil {
   177  					return err
   178  				}
   179  				for _, peer := range gg.Peers {
   180  					if peer.NetAddress == ggb.NetAddress {
   181  						aToB = true
   182  						break
   183  					}
   184  				}
   185  				if !aToB || !bToA {
   186  					return fmt.Errorf("called connect between two nodes, but they are not peers: %v %v %v %v %v %v", aToB, bToA, gg.NetAddress, ggb.NetAddress, gg.Peers, ggb.Peers)
   187  				}
   188  				return nil
   189  
   190  			})
   191  			if err != nil {
   192  				return err
   193  			}
   194  		}
   195  	}
   196  
   197  	// Perform a synchronization check.
   198  	_, err := synchronizationCheck(sts)
   199  	return err
   200  }
   201  
   202  // fundAllNodes will make sure that each node has mined a block in the longest
   203  // chain, then will mine enough blocks that the miner payouts manifest in the
   204  // wallets of each node.
   205  func fundAllNodes(sts []*serverTester) error {
   206  	// Check that all of the nodes are synchronized.
   207  	chainTip, err := synchronizationCheck(sts)
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	// Mine a block for each node to fund their wallet.
   213  	for i := range sts {
   214  		err := waitForBlock(chainTip, sts[i])
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		// Mine a block. The next iteration of this loop will ensure that the
   220  		// block propagates and does not get orphaned.
   221  		block, err := sts[i].miner.AddBlock()
   222  		if err != nil {
   223  			return err
   224  		}
   225  		chainTip = block.ID()
   226  	}
   227  
   228  	// Wait until the chain tip has propagated to the first node.
   229  	err = waitForBlock(chainTip, sts[0])
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	// Mine types.MaturityDelay more blocks from the final node to mine a
   235  	// block, to guarantee that all nodes have had their payouts mature, such
   236  	// that their wallets can begin spending immediately.
   237  	for i := types.BlockHeight(0); i <= types.MaturityDelay; i++ {
   238  		_, err := sts[0].miner.AddBlock()
   239  		if err != nil {
   240  			return err
   241  		}
   242  	}
   243  
   244  	// Block until every node has the full chain.
   245  	_, err = synchronizationCheck(sts)
   246  	return err
   247  }
   248  
   249  // synchronizationCheck takes a bunch of server testers as input and checks
   250  // that they all have the same current block as the first server tester. The
   251  // first server tester needs to have the most recent block in order for the
   252  // check to work.
   253  func synchronizationCheck(sts []*serverTester) (types.BlockID, error) {
   254  	// Prefer returning an error in the event of a zero-length server tester -
   255  	// an error should be returned if the developer accidentally uses a nil
   256  	// slice instead of whatever value was intended, and there's no reason to
   257  	// check for synchronization if there aren't any nodes to be synchronized.
   258  	if len(sts) == 0 {
   259  		return types.BlockID{}, errors.New("no server testers provided")
   260  	}
   261  
   262  	// Wait until all nodes are on the same block
   263  	for _, st := range sts[1:] {
   264  		err := waitForBlock(sts[0].cs.CurrentBlock().ID(), st)
   265  		if err != nil {
   266  			return types.BlockID{}, err
   267  		}
   268  	}
   269  
   270  	var cg ConsensusGET
   271  	err := sts[0].getAPI("/consensus", &cg)
   272  	if err != nil {
   273  		return types.BlockID{}, err
   274  	}
   275  	leaderBlockID := cg.CurrentBlock
   276  	for i := range sts {
   277  		// Spin until the current block matches the leader block.
   278  		success := false
   279  		for j := 0; j < 100; j++ {
   280  			err = sts[i].getAPI("/consensus", &cg)
   281  			if err != nil {
   282  				return types.BlockID{}, err
   283  			}
   284  			if cg.CurrentBlock == leaderBlockID {
   285  				success = true
   286  				break
   287  			}
   288  			time.Sleep(time.Millisecond * 100)
   289  		}
   290  		if !success {
   291  			return types.BlockID{}, errors.New("synchronization check failed - nodes do not seem to be synchronized")
   292  		}
   293  	}
   294  	return leaderBlockID, nil
   295  }
   296  
   297  // waitForBlock will block until the provided chain tip is the most recent
   298  // block in the provided testing node.
   299  func waitForBlock(chainTip types.BlockID, st *serverTester) error {
   300  	var cg ConsensusGET
   301  	success := false
   302  	for j := 0; j < 100; j++ {
   303  		err := st.getAPI("/consensus", &cg)
   304  		if err != nil {
   305  			return err
   306  		}
   307  		if cg.CurrentBlock == chainTip {
   308  			success = true
   309  			break
   310  		}
   311  		time.Sleep(time.Millisecond * 100)
   312  	}
   313  	if !success {
   314  		return errors.New("node never reached the correct chain tip")
   315  	}
   316  	return nil
   317  }