github.com/status-im/status-go@v1.1.0/timesource/timesource.go (about) 1 package timesource 2 3 import ( 4 "bytes" 5 "errors" 6 "sort" 7 "sync" 8 "time" 9 10 "github.com/beevik/ntp" 11 12 "github.com/ethereum/go-ethereum/log" 13 ) 14 15 const ( 16 // DefaultMaxAllowedFailures defines how many failures will be tolerated. 17 DefaultMaxAllowedFailures = 4 18 19 // FastNTPSyncPeriod period between ntp synchronizations before the first 20 // successful connection. 21 FastNTPSyncPeriod = 2 * time.Minute 22 23 // SlowNTPSyncPeriod period between ntp synchronizations after the first 24 // successful connection. 25 SlowNTPSyncPeriod = 1 * time.Hour 26 27 // DefaultRPCTimeout defines write deadline for single ntp server request. 28 DefaultRPCTimeout = 2 * time.Second 29 ) 30 31 // defaultServers will be resolved to the closest available, 32 // and with high probability resolved to the different IPs 33 var defaultServers = []string{ 34 "time.apple.com", 35 "pool.ntp.org", 36 "time.cloudflare.com", 37 "time.windows.com", 38 "ntp.neu.edu.cn", 39 "ntp.nict.jp", 40 "amazon.pool.ntp.org", 41 "android.pool.ntp.org", 42 } 43 var errUpdateOffset = errors.New("failed to compute offset") 44 45 type ntpQuery func(string, ntp.QueryOptions) (*ntp.Response, error) 46 47 type queryResponse struct { 48 Offset time.Duration 49 Error error 50 } 51 52 type multiRPCError []error 53 54 func (e multiRPCError) Error() string { 55 var b bytes.Buffer 56 b.WriteString("RPC failed: ") 57 more := false 58 for _, err := range e { 59 if more { 60 b.WriteString("; ") 61 } 62 b.WriteString(err.Error()) 63 more = true 64 } 65 b.WriteString(".") 66 return b.String() 67 } 68 69 func computeOffset(timeQuery ntpQuery, servers []string, allowedFailures int) (time.Duration, error) { 70 if len(servers) == 0 { 71 return 0, nil 72 } 73 responses := make(chan queryResponse, len(servers)) 74 for _, server := range servers { 75 go func(server string) { 76 response, err := timeQuery(server, ntp.QueryOptions{ 77 Timeout: DefaultRPCTimeout, 78 }) 79 if err == nil { 80 err = response.Validate() 81 } 82 if err != nil { 83 responses <- queryResponse{Error: err} 84 return 85 } 86 responses <- queryResponse{Offset: response.ClockOffset} 87 }(server) 88 } 89 var ( 90 rpcErrors multiRPCError 91 offsets []time.Duration 92 collected int 93 ) 94 for response := range responses { 95 if response.Error != nil { 96 rpcErrors = append(rpcErrors, response.Error) 97 } else { 98 offsets = append(offsets, response.Offset) 99 } 100 collected++ 101 if collected == len(servers) { 102 break 103 } 104 } 105 if lth := len(rpcErrors); lth > allowedFailures { 106 return 0, rpcErrors 107 } else if lth == len(servers) { 108 return 0, rpcErrors 109 } 110 sort.SliceStable(offsets, func(i, j int) bool { 111 return offsets[i] > offsets[j] 112 }) 113 mid := len(offsets) / 2 114 if len(offsets)%2 == 0 { 115 return (offsets[mid-1] + offsets[mid]) / 2, nil 116 } 117 return offsets[mid], nil 118 } 119 120 var defaultTimeSource = &NTPTimeSource{ 121 servers: defaultServers, 122 allowedFailures: DefaultMaxAllowedFailures, 123 fastNTPSyncPeriod: FastNTPSyncPeriod, 124 slowNTPSyncPeriod: SlowNTPSyncPeriod, 125 timeQuery: ntp.QueryWithOptions, 126 now: time.Now, 127 } 128 129 // Default initializes time source with default config values. 130 func Default() *NTPTimeSource { 131 return defaultTimeSource 132 } 133 134 // NTPTimeSource provides source of time that tries to be resistant to time skews. 135 // It does so by periodically querying time offset from ntp servers. 136 type NTPTimeSource struct { 137 servers []string 138 allowedFailures int 139 fastNTPSyncPeriod time.Duration 140 slowNTPSyncPeriod time.Duration 141 timeQuery ntpQuery // for ease of testing 142 now func() time.Time 143 144 quit chan struct{} 145 started bool 146 147 mu sync.RWMutex 148 latestOffset time.Duration 149 } 150 151 // Now returns time adjusted by latest known offset 152 func (s *NTPTimeSource) Now() time.Time { 153 s.mu.RLock() 154 defer s.mu.RUnlock() 155 n := s.now() 156 return n.Add(s.latestOffset) 157 } 158 159 func (s *NTPTimeSource) updateOffset() error { 160 offset, err := computeOffset(s.timeQuery, s.servers, s.allowedFailures) 161 if err != nil { 162 log.Error("failed to compute offset", "error", err) 163 return errUpdateOffset 164 } 165 log.Info("Difference with ntp servers", "offset", offset) 166 s.mu.Lock() 167 defer s.mu.Unlock() 168 s.latestOffset = offset 169 170 return nil 171 } 172 173 // runPeriodically runs periodically the given function based on NTPTimeSource 174 // synchronization limits (fastNTPSyncPeriod / slowNTPSyncPeriod) 175 func (s *NTPTimeSource) runPeriodically(fn func() error, starWithSlowSyncPeriod bool) { 176 if s.started { 177 return 178 } 179 180 period := s.fastNTPSyncPeriod 181 if starWithSlowSyncPeriod { 182 period = s.slowNTPSyncPeriod 183 } 184 s.quit = make(chan struct{}) 185 go func() { 186 for { 187 select { 188 case <-time.After(period): 189 if err := fn(); err == nil { 190 period = s.slowNTPSyncPeriod 191 } else if period != s.slowNTPSyncPeriod { 192 period = s.fastNTPSyncPeriod 193 } 194 195 case <-s.quit: 196 return 197 } 198 } 199 }() 200 } 201 202 // Start initializes the local offset and starts a goroutine that periodically updates the local offset. 203 func (s *NTPTimeSource) Start() { 204 if s.started { 205 return 206 } 207 208 // Attempt to update the offset synchronously so that user can have reliable messages right away 209 err := s.updateOffset() 210 if err != nil { 211 // Failure to update can occur if the node is offline. 212 // Instead of returning an error, continue with the process as the update will be retried periodically. 213 log.Error("failed to update offset", err) 214 } 215 216 s.runPeriodically(s.updateOffset, err == nil) 217 218 s.started = true 219 } 220 221 // Stop goroutine that updates time source. 222 func (s *NTPTimeSource) Stop() error { 223 if s.quit == nil { 224 return nil 225 } 226 close(s.quit) 227 s.started = false 228 return nil 229 } 230 231 func (s *NTPTimeSource) GetCurrentTime() time.Time { 232 s.Start() 233 return s.Now() 234 } 235 236 func (s *NTPTimeSource) GetCurrentTimeInMillis() uint64 { 237 return convertToMillis(s.GetCurrentTime()) 238 } 239 240 func GetCurrentTime() time.Time { 241 ts := Default() 242 ts.Start() 243 return ts.Now() 244 } 245 246 func GetCurrentTimeInMillis() uint64 { 247 return convertToMillis(GetCurrentTime()) 248 } 249 250 func convertToMillis(t time.Time) uint64 { 251 return uint64(t.UnixNano() / int64(time.Millisecond)) 252 }