package main import ( "context" "errors" "fmt" "time" rpchttp "github.com/tendermint/tendermint/rpc/client/http" rpctypes "github.com/tendermint/tendermint/rpc/coretypes" e2e "github.com/tendermint/tendermint/test/e2e/pkg" "github.com/tendermint/tendermint/types" ) // waitForHeight waits for the network to reach a certain height (or above), // returning the block at the height seen. Errors if the network is not making // progress at all. // If height == 0, the initial height of the test network is used as the target. func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*types.Block, *types.BlockID, error) { var ( err error clients = map[string]*rpchttp.HTTP{} lastHeight int64 lastIncrease = time.Now() nodesAtHeight = map[string]struct{}{} numRunningNodes int ) if height == 0 { height = testnet.InitialHeight } for _, node := range testnet.Nodes { if node.Stateless() { continue } if node.HasStarted { numRunningNodes++ } } timer := time.NewTimer(0) defer timer.Stop() for { select { case <-ctx.Done(): return nil, nil, ctx.Err() case <-timer.C: for _, node := range testnet.Nodes { // skip nodes that have reached the target height if _, ok := nodesAtHeight[node.Name]; ok { continue } // skip nodes that don't have state or haven't started yet if node.Stateless() { continue } if !node.HasStarted { continue } // cache the clients client, ok := clients[node.Name] if !ok { client, err = node.Client() if err != nil { continue } clients[node.Name] = client } result, err := client.Status(ctx) if err != nil { continue } if result.SyncInfo.LatestBlockHeight > lastHeight { lastHeight = result.SyncInfo.LatestBlockHeight lastIncrease = time.Now() } if result.SyncInfo.LatestBlockHeight >= height { // the node has achieved the target height! // add this node to the set of target // height nodes nodesAtHeight[node.Name] = struct{}{} // if not all of the nodes that we // have clients for have reached the // target height, keep trying. if numRunningNodes > len(nodesAtHeight) { continue } // All nodes are at or above the target height. Now fetch the block for that target height // and return it. We loop again through all clients because some may have pruning set but // at least two of them should be archive nodes. for _, c := range clients { result, err := c.Block(ctx, &height) if err != nil || result == nil || result.Block == nil { continue } return result.Block, &result.BlockID, err } } } if len(clients) == 0 { return nil, nil, errors.New("unable to connect to any network nodes") } if time.Since(lastIncrease) >= time.Minute { if lastHeight == 0 { return nil, nil, errors.New("chain stalled at unknown height (most likely upon starting)") } return nil, nil, fmt.Errorf("chain stalled at height %v [%d of %d nodes %+v]", lastHeight, len(nodesAtHeight), numRunningNodes, nodesAtHeight) } timer.Reset(1 * time.Second) } } } // waitForNode waits for a node to become available and catch up to the given block height. func waitForNode(ctx context.Context, node *e2e.Node, height int64) (*rpctypes.ResultStatus, error) { // If the node is the light client or seed note, we do not check for the last height. // The light client and seed note can be behind the full node and validator if node.Mode == e2e.ModeSeed { return nil, nil } client, err := node.Client() if err != nil { return nil, err } timer := time.NewTimer(0) defer timer.Stop() var ( lastFailed bool counter int ) for { counter++ if lastFailed { lastFailed = false // if there was a problem with the request in // the previous recreate the client to ensure // reconnection client, err = node.Client() if err != nil { return nil, err } } select { case <-ctx.Done(): return nil, ctx.Err() case <-timer.C: status, err := client.Status(ctx) switch { case errors.Is(err, context.DeadlineExceeded): return nil, fmt.Errorf("timed out waiting for %v to reach height %v", node.Name, height) case errors.Is(err, context.Canceled): return nil, err // If the node is the light client, it is not essential to wait for it to catch up, but we must return status info case err == nil && node.Mode == e2e.ModeLight: return status, nil case err == nil && node.Mode != e2e.ModeLight && status.SyncInfo.LatestBlockHeight >= height: return status, nil case counter%500 == 0: switch { case err != nil: lastFailed = true logger.Error("node not yet ready", "iter", counter, "node", node.Name, "target", height, "err", err, ) case status != nil: logger.Info("node not yet ready", "iter", counter, "node", node.Name, "height", status.SyncInfo.LatestBlockHeight, "target", height, ) } } timer.Reset(250 * time.Millisecond) } } } // getLatestBlock returns the last block that all active nodes in the network have // agreed upon i.e. the earlist of each nodes latest block func getLatestBlock(ctx context.Context, testnet *e2e.Testnet) (*types.Block, error) { var earliestBlock *types.Block for _, node := range testnet.Nodes { // skip nodes that don't have state or haven't started yet if node.Stateless() { continue } if !node.HasStarted { continue } client, err := node.Client() if err != nil { return nil, err } wctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() result, err := client.Block(wctx, nil) if err != nil { return nil, err } if result.Block != nil && (earliestBlock == nil || earliestBlock.Height > result.Block.Height) { earliestBlock = result.Block } } return earliestBlock, nil }