github.com/lbryio/lbcd@v0.22.119/blockchain/mediantime.go (about)

     1  // Copyright (c) 2013-2014 The btcsuite developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package blockchain
     6  
     7  import (
     8  	"math"
     9  	"sort"
    10  	"sync"
    11  	"time"
    12  )
    13  
    14  const (
    15  	// maxAllowedOffsetSeconds is the maximum number of seconds in either
    16  	// direction that local clock will be adjusted.  When the median time
    17  	// of the network is outside of this range, no offset will be applied.
    18  	maxAllowedOffsetSecs = 70 * 60 // 1 hour 10 minutes
    19  
    20  	// similarTimeSecs is the number of seconds in either direction from the
    21  	// local clock that is used to determine that it is likely wrong and
    22  	// hence to show a warning.
    23  	similarTimeSecs = 5 * 60 // 5 minutes
    24  )
    25  
    26  var (
    27  	// maxMedianTimeEntries is the maximum number of entries allowed in the
    28  	// median time data.  This is a variable as opposed to a constant so the
    29  	// test code can modify it.
    30  	maxMedianTimeEntries = 200
    31  )
    32  
    33  // MedianTimeSource provides a mechanism to add several time samples which are
    34  // used to determine a median time which is then used as an offset to the local
    35  // clock.
    36  type MedianTimeSource interface {
    37  	// AdjustedTime returns the current time adjusted by the median time
    38  	// offset as calculated from the time samples added by AddTimeSample.
    39  	AdjustedTime() time.Time
    40  
    41  	// AddTimeSample adds a time sample that is used when determining the
    42  	// median time of the added samples.
    43  	AddTimeSample(id string, timeVal time.Time)
    44  
    45  	// Offset returns the number of seconds to adjust the local clock based
    46  	// upon the median of the time samples added by AddTimeData.
    47  	Offset() time.Duration
    48  }
    49  
    50  // int64Sorter implements sort.Interface to allow a slice of 64-bit integers to
    51  // be sorted.
    52  type int64Sorter []int64
    53  
    54  // Len returns the number of 64-bit integers in the slice.  It is part of the
    55  // sort.Interface implementation.
    56  func (s int64Sorter) Len() int {
    57  	return len(s)
    58  }
    59  
    60  // Swap swaps the 64-bit integers at the passed indices.  It is part of the
    61  // sort.Interface implementation.
    62  func (s int64Sorter) Swap(i, j int) {
    63  	s[i], s[j] = s[j], s[i]
    64  }
    65  
    66  // Less returns whether the 64-bit integer with index i should sort before the
    67  // 64-bit integer with index j.  It is part of the sort.Interface
    68  // implementation.
    69  func (s int64Sorter) Less(i, j int) bool {
    70  	return s[i] < s[j]
    71  }
    72  
    73  // medianTime provides an implementation of the MedianTimeSource interface.
    74  // It is limited to maxMedianTimeEntries includes the same buggy behavior as
    75  // the time offset mechanism in Bitcoin Core.  This is necessary because it is
    76  // used in the consensus code.
    77  type medianTime struct {
    78  	mtx                sync.Mutex
    79  	knownIDs           map[string]struct{}
    80  	offsets            []int64
    81  	offsetSecs         int64
    82  	invalidTimeChecked bool
    83  }
    84  
    85  // Ensure the medianTime type implements the MedianTimeSource interface.
    86  var _ MedianTimeSource = (*medianTime)(nil)
    87  
    88  // AdjustedTime returns the current time adjusted by the median time offset as
    89  // calculated from the time samples added by AddTimeSample.
    90  //
    91  // This function is safe for concurrent access and is part of the
    92  // MedianTimeSource interface implementation.
    93  func (m *medianTime) AdjustedTime() time.Time {
    94  	m.mtx.Lock()
    95  	defer m.mtx.Unlock()
    96  
    97  	// Limit the adjusted time to 1 second precision.
    98  	now := time.Unix(time.Now().Unix(), 0)
    99  	return now.Add(time.Duration(m.offsetSecs) * time.Second)
   100  }
   101  
   102  // AddTimeSample adds a time sample that is used when determining the median
   103  // time of the added samples.
   104  //
   105  // This function is safe for concurrent access and is part of the
   106  // MedianTimeSource interface implementation.
   107  func (m *medianTime) AddTimeSample(sourceID string, timeVal time.Time) {
   108  	m.mtx.Lock()
   109  	defer m.mtx.Unlock()
   110  
   111  	// Don't add time data from the same source.
   112  	if _, exists := m.knownIDs[sourceID]; exists {
   113  		return
   114  	}
   115  	m.knownIDs[sourceID] = struct{}{}
   116  
   117  	// Truncate the provided offset to seconds and append it to the slice
   118  	// of offsets while respecting the maximum number of allowed entries by
   119  	// replacing the oldest entry with the new entry once the maximum number
   120  	// of entries is reached.
   121  	now := time.Unix(time.Now().Unix(), 0)
   122  	offsetSecs := int64(timeVal.Sub(now).Seconds())
   123  	numOffsets := len(m.offsets)
   124  	if numOffsets == maxMedianTimeEntries && maxMedianTimeEntries > 0 {
   125  		m.offsets = m.offsets[1:]
   126  		numOffsets--
   127  	}
   128  	m.offsets = append(m.offsets, offsetSecs)
   129  	numOffsets++
   130  
   131  	// Sort the offsets so the median can be obtained as needed later.
   132  	sortedOffsets := make([]int64, numOffsets)
   133  	copy(sortedOffsets, m.offsets)
   134  	sort.Sort(int64Sorter(sortedOffsets))
   135  
   136  	offsetDuration := time.Duration(offsetSecs) * time.Second
   137  	log.Debugf("Added time sample of %v (total: %v)", offsetDuration,
   138  		numOffsets)
   139  
   140  	// NOTE: The following code intentionally has a bug to mirror the
   141  	// buggy behavior in Bitcoin Core since the median time is used in the
   142  	// consensus rules.
   143  	//
   144  	// In particular, the offset is only updated when the number of entries
   145  	// is odd, but the max number of entries is 200, an even number.  Thus,
   146  	// the offset will never be updated again once the max number of entries
   147  	// is reached.
   148  
   149  	// The median offset is only updated when there are enough offsets and
   150  	// the number of offsets is odd so the middle value is the true median.
   151  	// Thus, there is nothing to do when those conditions are not met.
   152  	if numOffsets < 5 || numOffsets&0x01 != 1 {
   153  		return
   154  	}
   155  
   156  	// At this point the number of offsets in the list is odd, so the
   157  	// middle value of the sorted offsets is the median.
   158  	median := sortedOffsets[numOffsets/2]
   159  
   160  	// Set the new offset when the median offset is within the allowed
   161  	// offset range.
   162  	if math.Abs(float64(median)) < maxAllowedOffsetSecs {
   163  		m.offsetSecs = median
   164  	} else {
   165  		// The median offset of all added time data is larger than the
   166  		// maximum allowed offset, so don't use an offset.  This
   167  		// effectively limits how far the local clock can be skewed.
   168  		m.offsetSecs = 0
   169  
   170  		if !m.invalidTimeChecked {
   171  			m.invalidTimeChecked = true
   172  
   173  			// Find if any time samples have a time that is close
   174  			// to the local time.
   175  			var remoteHasCloseTime bool
   176  			for _, offset := range sortedOffsets {
   177  				if math.Abs(float64(offset)) < similarTimeSecs {
   178  					remoteHasCloseTime = true
   179  					break
   180  				}
   181  			}
   182  
   183  			// Warn if none of the time samples are close.
   184  			if !remoteHasCloseTime {
   185  				log.Warnf("Please check your date and time " +
   186  					"are correct!  lbcd will not work " +
   187  					"properly with an invalid time")
   188  			}
   189  		}
   190  	}
   191  
   192  	medianDuration := time.Duration(m.offsetSecs) * time.Second
   193  	log.Debugf("New time offset: %v", medianDuration)
   194  }
   195  
   196  // Offset returns the number of seconds to adjust the local clock based upon the
   197  // median of the time samples added by AddTimeData.
   198  //
   199  // This function is safe for concurrent access and is part of the
   200  // MedianTimeSource interface implementation.
   201  func (m *medianTime) Offset() time.Duration {
   202  	m.mtx.Lock()
   203  	defer m.mtx.Unlock()
   204  
   205  	return time.Duration(m.offsetSecs) * time.Second
   206  }
   207  
   208  // NewMedianTime returns a new instance of concurrency-safe implementation of
   209  // the MedianTimeSource interface.  The returned implementation contains the
   210  // rules necessary for proper time handling in the chain consensus rules and
   211  // expects the time samples to be added from the timestamp field of the version
   212  // message received from remote peers that successfully connect and negotiate.
   213  func NewMedianTime() MedianTimeSource {
   214  	return &medianTime{
   215  		knownIDs: make(map[string]struct{}),
   216  		offsets:  make([]int64, 0, maxMedianTimeEntries),
   217  	}
   218  }