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  }