diff --git a/chain.go b/chain.go index 0b979dcc..9066a2d6 100644 --- a/chain.go +++ b/chain.go @@ -1038,7 +1038,7 @@ func (b *BlockChain) connectBestChain(node *blockNode, block *btcutil.Block, fla // - Latest block has a timestamp newer than 24 hours ago // // This function is NOT safe for concurrent access. -func (b *BlockChain) IsCurrent() bool { +func (b *BlockChain) IsCurrent(timeSource MedianTimeSource) bool { // Not current if there isn't a main (best) chain yet. if b.bestChain == nil { return false @@ -1053,8 +1053,8 @@ func (b *BlockChain) IsCurrent() bool { // Not current if the latest best block has a timestamp before 24 hours // ago. - now := time.Now() - if b.bestChain.timestamp.Before(now.Add(-24 * time.Hour)) { + minus24Hours := timeSource.AdjustedTime().Add(-24 * time.Hour) + if b.bestChain.timestamp.Before(minus24Hours) { return false } diff --git a/chain_test.go b/chain_test.go index b1e4d72e..22f0d01b 100644 --- a/chain_test.go +++ b/chain_test.go @@ -48,8 +48,10 @@ func TestHaveBlock(t *testing.T) { chain.DisableCheckpoints(true) btcchain.TstSetCoinbaseMaturity(1) + timeSource := btcchain.NewMedianTime() for i := 1; i < len(blocks); i++ { - isOrphan, err := chain.ProcessBlock(blocks[i], btcchain.BFNone) + isOrphan, err := chain.ProcessBlock(blocks[i], timeSource, + btcchain.BFNone) if err != nil { t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) return @@ -62,7 +64,8 @@ func TestHaveBlock(t *testing.T) { } // Insert an orphan block. - isOrphan, err := chain.ProcessBlock(btcutil.NewBlock(&Block100000), btcchain.BFNone) + isOrphan, err := chain.ProcessBlock(btcutil.NewBlock(&Block100000), + timeSource, btcchain.BFNone) if err != nil { t.Errorf("Unable to process block: %v", err) return diff --git a/example_test.go b/example_test.go index d23c9476..08f62991 100644 --- a/example_test.go +++ b/example_test.go @@ -45,10 +45,16 @@ func ExampleBlockChain_ProcessBlock() { // the main bitcoin network and ignore notifications. chain := btcchain.New(db, &btcnet.MainNetParams, nil) + // Create a new median time source that is required by the upcoming + // call to ProcessBlock. Ordinarily this would also add time values + // obtained from other peers on the network so the local time is + // adjusted to be in agreement with other peers. + timeSource := btcchain.NewMedianTime() + // Process a block. For this example, we are going to intentionally // cause an error by trying to process the genesis block which already // exists. - isOrphan, err := chain.ProcessBlock(genesisBlock, btcchain.BFNone) + isOrphan, err := chain.ProcessBlock(genesisBlock, timeSource, btcchain.BFNone) if err != nil { fmt.Printf("Failed to process block: %v\n", err) return diff --git a/mediantime.go b/mediantime.go new file mode 100644 index 00000000..b11fcd34 --- /dev/null +++ b/mediantime.go @@ -0,0 +1,217 @@ +// Copyright (c) 2013-2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcchain + +import ( + "math" + "sort" + "sync" + "time" +) + +const ( + // maxAllowedOffsetSeconds is the maximum number of seconds in either + // direction that local clock will be adjusted. When the median time + // of the network is outside of this range, no offset will be applied. + maxAllowedOffsetSecs = 70 * 60 // 1 hour 10 minutes + + // similarTimeSecs is the number of seconds in either direction from the + // local clock that is used to determine that it is likley wrong and + // hence to show a warning. + similarTimeSecs = 5 * 60 // 5 minutes +) + +var ( + // maxMedianTimeEntries is the maximum number of entries allowed in the + // median time data. This is a variable as opposed to a constant so the + // test code can modify it. + maxMedianTimeEntries = 200 +) + +// MedianTimeSource provides a mechanism to add several time samples which are +// used to determine a median time which is then used as an offset to the local +// clock. +type MedianTimeSource interface { + // AdjustedTime returns the current time adjusted by the median time + // offset as calculated from the time samples added by AddTimeSample. + AdjustedTime() time.Time + + // AddTimeSample adds a time sample that is used when determining the + // median time of the added samples. + AddTimeSample(id string, timeVal time.Time) + + // Offset returns the number of seconds to adjust the local clock based + // upon the median of the time samples added by AddTimeData. + Offset() time.Duration +} + +// int64Sorter implements sort.Interface to allow a slice of 64-bit integers to +// be sorted. +type int64Sorter []int64 + +// Len returns the number of 64-bit integers in the slice. It is part of the +// sort.Interface implementation. +func (s int64Sorter) Len() int { + return len(s) +} + +// Swap swaps the 64-bit integers at the passed indices. It is part of the +// sort.Interface implementation. +func (s int64Sorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less returns whether the 64-bit integer with index i should sort before the +// 64-bit integer with index j. It is part of the sort.Interface +// implementation. +func (s int64Sorter) Less(i, j int) bool { + return s[i] < s[j] +} + +// medianTime provides an implementation of the MedianTimeSource interface. +// It is limited to maxMedianTimeEntries includes the same buggy behavior as +// the time offset mechanism in Bitcoin Core. This is necessary because it is +// used in the consensus code. +type medianTime struct { + mtx sync.Mutex + knownIDs map[string]struct{} + offsets []int64 + offsetSecs int64 + invalidTimeChecked bool +} + +// Ensure the medianTime type implements the MedianTimeSource interface. +var _ MedianTimeSource = (*medianTime)(nil) + +// AdjustedTime returns the current time adjusted by the median time offset as +// calculated from the time samples added by AddTimeSample. +// +// This function is safe for concurrent access and is part of the +// MedianTimeSource interface implementation. +func (m *medianTime) AdjustedTime() time.Time { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Limit the adjusted time to 1 second precision. + now := time.Unix(time.Now().Unix(), 0) + return now.Add(time.Duration(m.offsetSecs) * time.Second) +} + +// AddTimeSample adds a time sample that is used when determining the median +// time of the added samples. +// +// This function is safe for concurrent access and is part of the +// MedianTimeSource interface implementation. +func (m *medianTime) AddTimeSample(sourceID string, timeVal time.Time) { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Don't add time data from the same source. + if _, exists := m.knownIDs[sourceID]; exists { + return + } + m.knownIDs[sourceID] = struct{}{} + + // Truncate the provided offset to seconds and append it to the slice + // of offsets while respecting the maximum number of allowed entries by + // replacing the oldest entry with the new entry once the maximum number + // of entries is reached. + offsetSecs := int64(time.Now().Sub(timeVal).Seconds()) + numOffsets := len(m.offsets) + if numOffsets == maxMedianTimeEntries && maxMedianTimeEntries > 0 { + m.offsets = m.offsets[1:] + numOffsets-- + } + m.offsets = append(m.offsets, offsetSecs) + numOffsets++ + + // Sort the offsets so the median can be obtained as needed later. + sortedOffsets := make([]int64, numOffsets) + copy(sortedOffsets, m.offsets) + sort.Sort(int64Sorter(sortedOffsets)) + + offsetDuration := time.Duration(offsetSecs) * time.Second + log.Debugf("Added time sample of %v (total: %v)", offsetDuration, + numOffsets) + + // NOTE: The following code intentionally has a bug to mirror the + // buggy behavior in Bitcoin Core since the median time is used in the + // consensus rules. + // + // In particular, the offset is only updated when the number of entries + // is odd, but the max number of entries is 200, an even number. Thus, + // the offset will never be updated again once the max number of entries + // is reached. + + // The median offset is only updated when there are enough offsets and + // the number of offsets is odd so the middle value is the true median. + // Thus, there is nothing to do when those conditions are not met. + if numOffsets < 5 || numOffsets&0x01 != 1 { + return + } + + // At this point the number of offsets in the list is odd, so the + // middle value of the sorted offsets is the median. + median := sortedOffsets[numOffsets/2] + + // Set the new offset when the median offset is within the allowed + // offset range. + if math.Abs(float64(median)) < maxAllowedOffsetSecs { + m.offsetSecs = median + } else { + // The median offset of all added time data is larger than the + // maximum allowed offset, so don't use an offset. This + // effectively limits how far the local clock can be skewed. + m.offsetSecs = 0 + + if !m.invalidTimeChecked { + m.invalidTimeChecked = true + + // Find if any time samples have a time that is close + // to the local time. + var remoteHasCloseTime bool + for _, offset := range sortedOffsets { + if math.Abs(float64(offset)) < similarTimeSecs { + remoteHasCloseTime = true + break + } + } + + // Warn if none of the time samples are close. + if !remoteHasCloseTime { + log.Warnf("Please check your date and time " + + "are correct! btcd will not work " + + "properly with an invalid time") + } + } + } + + medianDuration := time.Duration(m.offsetSecs) * time.Second + log.Debugf("New time offset: %v", medianDuration) +} + +// Offset returns the number of seconds to adjust the local clock based upon the +// median of the time samples added by AddTimeData. +// +// This function is safe for concurrent access and is part of the +// MedianTimeSource interface implementation. +func (m *medianTime) Offset() time.Duration { + m.mtx.Lock() + defer m.mtx.Unlock() + + return time.Duration(m.offsetSecs) * time.Second +} + +// NewMedianTime returns a new instance of concurrency-safe implementation of +// the MedianTimeSource interface. The returned implementation contains the +// rules necessary for proper time handling in the chain consensus rules and +// expects the time samples to be added from the timestamp field of the version +// message received from remote peers that successfully connect and negotiate. +func NewMedianTime() MedianTimeSource { + return &medianTime{ + knownIDs: make(map[string]struct{}), + offsets: make([]int64, 0, maxMedianTimeEntries), + } +} diff --git a/process.go b/process.go index 55927afd..d6162590 100644 --- a/process.go +++ b/process.go @@ -114,7 +114,7 @@ func (b *BlockChain) processOrphans(hash *btcwire.ShaHash, flags BehaviorFlags) // It returns a bool which indicates whether or not the block is an orphan and // any errors that occurred during processing. The returned bool is only valid // when the error is nil. -func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bool, error) { +func (b *BlockChain) ProcessBlock(block *btcutil.Block, timeSource MedianTimeSource, flags BehaviorFlags) (bool, error) { fastAdd := flags&BFFastAdd == BFFastAdd dryRun := flags&BFDryRun == BFDryRun @@ -141,7 +141,7 @@ func (b *BlockChain) ProcessBlock(block *btcutil.Block, flags BehaviorFlags) (bo } // Perform preliminary sanity checks on the block and its transactions. - err = checkBlockSanity(block, b.netParams.PowLimit, flags) + err = checkBlockSanity(block, b.netParams.PowLimit, timeSource, flags) if err != nil { return false, err } diff --git a/reorganization_test.go b/reorganization_test.go index d6baa87b..1acaf23b 100644 --- a/reorganization_test.go +++ b/reorganization_test.go @@ -58,9 +58,10 @@ func TestReorganization(t *testing.T) { chain.DisableCheckpoints(true) btcchain.TstSetCoinbaseMaturity(1) + timeSource := btcchain.NewMedianTime() expectedOrphans := map[int]struct{}{5: struct{}{}, 6: struct{}{}} for i := 1; i < len(blocks); i++ { - isOrphan, err := chain.ProcessBlock(blocks[i], btcchain.BFNone) + isOrphan, err := chain.ProcessBlock(blocks[i], timeSource, btcchain.BFNone) if err != nil { t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) return diff --git a/validate.go b/validate.go index 5f4bccc1..04cb1787 100644 --- a/validate.go +++ b/validate.go @@ -423,7 +423,7 @@ func CountP2SHSigOps(tx *btcutil.Tx, isCoinBaseTx bool, txStore TxStore) (int, e // // The flags do not modify the behavior of this function directly, however they // are needed to pass along to checkProofOfWork. -func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, flags BehaviorFlags) error { +func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, timeSource MedianTimeSource, flags BehaviorFlags) error { // A block must have at least one transaction. msgBlock := block.MsgBlock() numTx := len(msgBlock.Transactions) @@ -469,7 +469,8 @@ func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, flags BehaviorFla } // Ensure the block time is not too far in the future. - maxTimestamp := time.Now().Add(time.Second * MaxTimeOffsetSeconds) + maxTimestamp := timeSource.AdjustedTime().Add(time.Second * + MaxTimeOffsetSeconds) if header.Timestamp.After(maxTimestamp) { str := fmt.Sprintf("block timestamp of %v is too far in the "+ "future", header.Timestamp) @@ -551,8 +552,8 @@ func checkBlockSanity(block *btcutil.Block, powLimit *big.Int, flags BehaviorFla // CheckBlockSanity performs some preliminary checks on a block to ensure it is // sane before continuing with block processing. These checks are context free. -func CheckBlockSanity(block *btcutil.Block, powLimit *big.Int) error { - return checkBlockSanity(block, powLimit, BFNone) +func CheckBlockSanity(block *btcutil.Block, powLimit *big.Int, timeSource MedianTimeSource) error { + return checkBlockSanity(block, powLimit, timeSource, BFNone) } // checkSerializedHeight checks if the signature script in the passed diff --git a/validate_test.go b/validate_test.go index 7aa7f1e2..aa91333b 100644 --- a/validate_test.go +++ b/validate_test.go @@ -41,10 +41,13 @@ func TestCheckConnectBlock(t *testing.T) { } } +// TestCheckBlockSanity tests the CheckBlockSanity function to ensure it works +// as expected. func TestCheckBlockSanity(t *testing.T) { powLimit := btcnet.MainNetParams.PowLimit block := btcutil.NewBlock(&Block100000) - err := btcchain.CheckBlockSanity(block, powLimit) + timeSource := btcchain.NewMedianTime() + err := btcchain.CheckBlockSanity(block, powLimit, timeSource) if err != nil { t.Errorf("CheckBlockSanity: %v", err) } @@ -53,7 +56,7 @@ func TestCheckBlockSanity(t *testing.T) { // second fails. timestamp := block.MsgBlock().Header.Timestamp block.MsgBlock().Header.Timestamp = timestamp.Add(time.Nanosecond) - err = btcchain.CheckBlockSanity(block, powLimit) + err = btcchain.CheckBlockSanity(block, powLimit, timeSource) if err == nil { t.Errorf("CheckBlockSanity: error is nil when it shouldn't be") }