github.com/stellar/stellar-etl@v1.0.1-0.20240312145900-4874b6bf2b89/internal/input/ledger_range.go (about)

     1  package input
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/stellar/stellar-etl/internal/utils"
     8  
     9  	"github.com/stellar/go/historyarchive"
    10  )
    11  
    12  // graphPoint represents a single point in the graph. It includes the ledger sequence and close time (in UTC)
    13  type graphPoint struct {
    14  	Seq       int64
    15  	CloseTime time.Time
    16  }
    17  
    18  /*
    19  The graph struct is used to calculate ledger ranges from time ranges. It keeps track of its boundaries, and uses its backend to
    20  retrieve new graphPoints as necessary. As the sequence number increases, so does the close time, so we can use the graph to find
    21  sequence numbers that correspond to a given close time fairly easily.
    22  */
    23  type graph struct {
    24  	Client     historyarchive.ArchiveInterface
    25  	BeginPoint graphPoint
    26  	EndPoint   graphPoint
    27  }
    28  
    29  const avgCloseTime = time.Second * 5 // average time to close a stellar ledger
    30  
    31  // GetLedgerRange calculates the ledger range that spans the provided date range
    32  func GetLedgerRange(startTime, endTime time.Time, isTest bool, isFuture bool) (int64, int64, error) {
    33  	startTime = startTime.UTC()
    34  	endTime = endTime.UTC()
    35  	env := utils.GetEnvironmentDetails(isTest, isFuture)
    36  
    37  	if startTime.After(endTime) {
    38  		return 0, 0, fmt.Errorf("start time must be less than or equal to the end time")
    39  	}
    40  
    41  	graph, err := createNewGraph(env.ArchiveURLs)
    42  	if err != nil {
    43  		return 0, 0, err
    44  	}
    45  
    46  	err = graph.limitLedgerRange(&startTime, &endTime)
    47  	if err != nil {
    48  		return 0, 0, err
    49  	}
    50  
    51  	// Ledger sequence 2 is the start ledger because the genesis ledger (ledger 1), has a close time of 0 in Unix time.
    52  	// The second ledger has a valid close time that matches with the network start time.
    53  	startLedger, err := graph.findLedgerForDate(2, startTime, map[int64]struct{}{})
    54  	if err != nil {
    55  		return 0, 0, err
    56  	}
    57  
    58  	endLedger, err := graph.findLedgerForDate(2, endTime, map[int64]struct{}{})
    59  	if err != nil {
    60  		return 0, 0, err
    61  	}
    62  
    63  	return startLedger, endLedger, nil
    64  }
    65  
    66  // createNewGraph makes a new graph with the endpoints equal to the network's endpoints
    67  func createNewGraph(archiveURLs []string) (graph, error) {
    68  	graph := graph{}
    69  	archive, err := utils.CreateHistoryArchiveClient(archiveURLs)
    70  	if err != nil {
    71  		return graph, err
    72  	}
    73  
    74  	graph.Client = archive
    75  
    76  	secondLedgerPoint, err := graph.getGraphPoint(2) // the second ledger has a real close time, unlike the 1970s close time of the genesis ledger
    77  	if err != nil {
    78  		return graph, err
    79  	}
    80  
    81  	graph.BeginPoint = secondLedgerPoint
    82  
    83  	root, err := graph.Client.GetRootHAS()
    84  	if err != nil {
    85  		return graph, err
    86  	}
    87  
    88  	latestPoint, err := graph.getGraphPoint(int64(root.CurrentLedger))
    89  	if err != nil {
    90  		return graph, err
    91  	}
    92  
    93  	graph.EndPoint = latestPoint
    94  	return graph, nil
    95  }
    96  
    97  func (g graph) findLedgerForTimeBinary(targetTime time.Time, start, end graphPoint) (int64, error) {
    98  	if end.Seq >= 2 {
    99  		middleLedger := start.Seq + (end.Seq-start.Seq)/2
   100  		middleTime, err := g.getGraphPoint(middleLedger)
   101  		if err != nil {
   102  			return 0, err
   103  		}
   104  
   105  		// check if middle element is the one to choose
   106  		if middleLedger > 1 {
   107  			prevLedger := middleLedger - 1
   108  			prevTime, err := g.getGraphPoint(prevLedger)
   109  			if err != nil {
   110  				return 0, err
   111  			}
   112  
   113  			if prevTime.CloseTime.Unix() < targetTime.Unix() && middleTime.CloseTime.Unix() >= targetTime.Unix() {
   114  				return middleLedger, nil
   115  			}
   116  		}
   117  
   118  		if middleTime.CloseTime.Unix() > targetTime.Unix() {
   119  			newEnd, err := g.getGraphPoint(middleLedger - 1)
   120  			if err != nil {
   121  				return 0, err
   122  			}
   123  
   124  			return g.findLedgerForTimeBinary(targetTime, start, newEnd)
   125  		}
   126  
   127  		newStart, err := g.getGraphPoint(middleLedger + 1)
   128  		if err != nil {
   129  			return 0, err
   130  		}
   131  
   132  		return g.findLedgerForTimeBinary(targetTime, newStart, end)
   133  	}
   134  
   135  	return 0, fmt.Errorf("unable to find ledger with close time %v: ", targetTime)
   136  }
   137  
   138  // findLedgerForDate recursively searches for the ledger that was closed on or directly after targetTime
   139  func (g graph) findLedgerForDate(currentLedger int64, targetTime time.Time, seenLedgers map[int64]struct{}) (int64, error) {
   140  	seenLedgers[currentLedger] = struct{}{}
   141  
   142  	currentPoint, err := g.getGraphPoint(currentLedger)
   143  	if err != nil {
   144  		return 0, err
   145  	}
   146  
   147  	if currentLedger > 1 {
   148  		prevLedger := currentLedger - 1
   149  		prevTime, err := g.getGraphPoint(prevLedger)
   150  		if err != nil {
   151  			return 0, err
   152  		}
   153  
   154  		if prevTime.CloseTime.Unix() < targetTime.Unix() && currentPoint.CloseTime.Unix() >= targetTime.Unix() {
   155  			return currentLedger, nil
   156  		}
   157  	}
   158  
   159  	timeDiff := targetTime.Sub(currentPoint.CloseTime).Seconds()
   160  	ledgerOffset := int64(timeDiff / avgCloseTime.Seconds())
   161  	if ledgerOffset == 0 {
   162  		if timeDiff > 0 {
   163  			ledgerOffset = 1
   164  		} else {
   165  			ledgerOffset = -1
   166  		}
   167  	}
   168  
   169  	newLedger := currentLedger + ledgerOffset
   170  
   171  	if newLedger > g.EndPoint.Seq {
   172  		newLedger = g.EndPoint.Seq
   173  	} else if newLedger < g.BeginPoint.Seq {
   174  		// since we started with BeginPoint, returning to it would create an infinite cycle;
   175  		newLedger = g.BeginPoint.Seq + 1
   176  	}
   177  
   178  	// if we have already seen this ledger, it means the algorithm is trapped in a cycle; use binary search instead (slower but will find the ledger)
   179  	if _, exists := seenLedgers[newLedger]; exists {
   180  		// since we have already calculated the current point, we can use it as the upper or lower bound.
   181  		// This way, the binary search doesn't have to search the entire space
   182  		if ledgerOffset > 0 {
   183  			return g.findLedgerForTimeBinary(targetTime, currentPoint, g.EndPoint)
   184  		}
   185  
   186  		return g.findLedgerForTimeBinary(targetTime, g.BeginPoint, currentPoint)
   187  	}
   188  
   189  	return g.findLedgerForDate(newLedger, targetTime, seenLedgers)
   190  }
   191  
   192  // limitLedgerRange restricts start and end by setting them to be the edges of the network's range if they are outside that range
   193  func (g graph) limitLedgerRange(start, end *time.Time) error {
   194  	if start.Before(g.BeginPoint.CloseTime) {
   195  		*start = g.BeginPoint.CloseTime
   196  	} else if start.After(g.EndPoint.CloseTime) {
   197  		*start = g.EndPoint.CloseTime
   198  	}
   199  
   200  	if end.After(g.EndPoint.CloseTime) {
   201  		*end = g.EndPoint.CloseTime
   202  	} else if end.Before(g.BeginPoint.CloseTime) {
   203  		*end = g.BeginPoint.CloseTime
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  // getGraphPoint gets the graphPoint representation of the ledger with the provided sequence number
   210  func (g graph) getGraphPoint(sequence int64) (graphPoint, error) {
   211  	ledger, err := g.Client.GetLedgerHeader(uint32(sequence))
   212  	if err != nil {
   213  		return graphPoint{}, fmt.Errorf(fmt.Sprintf("unable to get ledger %d: ", sequence), err)
   214  	}
   215  
   216  	closeTime, err := utils.ExtractLedgerCloseTime(ledger)
   217  	if err != nil {
   218  		return graphPoint{}, fmt.Errorf(fmt.Sprintf("unable to extract close time from ledger %d: ", sequence), err)
   219  	}
   220  
   221  	return graphPoint{
   222  		Seq:       sequence,
   223  		CloseTime: closeTime,
   224  	}, nil
   225  }