Provide a new median time source API.

This commit provides a new interface, MedianTimeSource, along with a
concrete implementation which allows improved accuracy of time by making
use of the median network time as calculated from multiple time samples.

The time samples are to be provided by callers and are intended to come
from remote clients.

The calculations performed in this implementation exactly mirror those in
Bitcoin Core because time calculations are part of the consensus rules and
hence need to match exactly.
This commit is contained in:
Dave Collins 2014-10-03 15:29:25 -05:00
parent f60e503308
commit df065eee19
8 changed files with 246 additions and 15 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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

217
mediantime.go Normal file
View file

@ -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),
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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")
}