github.com/decred/dcrd/blockchain@v1.2.1/mediantime.go (about)

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