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 }