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 }