public void AddTimeData_WithLargeInboundSampleSet_ForgetsOldSamples() { var dateTimeProvider = new DateTimeProvider(); TimeSyncBehaviorState state = this.CreateTimeSyncBehaviorState(dateTimeProvider); // Add max inbound samples. for (int i = 0; i < TimeSyncBehaviorState.MaxInboundSamples; i++) { bool used = state.AddTimeData(new IPAddress(i), TimeSpan.FromSeconds(400), true); Assert.True(used); } // Add enough outbound samples to get an adjustment (and across an even range). Assert.True(state.AddTimeData(new IPAddress(1), TimeSpan.FromSeconds(-500), false)); Assert.True(state.AddTimeData(new IPAddress(2), TimeSpan.FromSeconds(-250), false)); Assert.True(state.AddTimeData(new IPAddress(3), TimeSpan.FromSeconds(250), false)); Assert.True(state.AddTimeData(new IPAddress(4), TimeSpan.FromSeconds(500), false)); TimeSpan diff = dateTimeProvider.GetAdjustedTime() - dateTimeProvider.GetUtcNow(); Assert.True(Math.Abs(diff.TotalMilliseconds - 250 * 1000) < TimeEpsilonMs, $"Should be 250ms because leaning towards the inbound value of 400ms, but still in outbound. Actual: {diff.TotalMilliseconds}ms"); // Add another batch of inbound samples with a greater offset than highest outbound sample. for (int i = TimeSyncBehaviorState.MaxInboundSamples; i < TimeSyncBehaviorState.MaxInboundSamples * 2; i++) { Assert.True(state.AddTimeData(new IPAddress(i * 2), TimeSpan.FromSeconds(-1000), true), $"index: {i}"); } diff = dateTimeProvider.GetAdjustedTime() - dateTimeProvider.GetUtcNow(); Assert.True(Math.Abs(diff.TotalMilliseconds - (-250 * 1000)) < TimeEpsilonMs, $"Should be -250ms because the new inbound samples replace the old and swing the median closer to the new -1000ms values, but still in outbound. Actual: {diff.TotalMilliseconds}ms"); }
public void AddTimeData_WithLargeOutboundSampleSet_ForgetsOldSamples() { var dateTimeProvider = new DateTimeProvider(); TimeSyncBehaviorState state = this.CreateTimeSyncBehaviorState(dateTimeProvider); // Add max outbound. for (int i = 0; i < TimeSyncBehaviorState.MaxOutboundSamples; i++) { var peerAddress = new IPAddress(i); bool used = state.AddTimeData(peerAddress, TimeSpan.FromSeconds(400), false); Assert.True(used); } TimeSpan diff = dateTimeProvider.GetAdjustedTime() - dateTimeProvider.GetUtcNow(); Assert.True(Math.Abs(diff.TotalMilliseconds - 400 * 1000) < TimeEpsilonMs, $"should be 400ms because of outbound preference. Actual: {diff.TotalMilliseconds}ms"); // Add another batch of outbounds with a different offset. for (int i = TimeSyncBehaviorState.MaxOutboundSamples; i < TimeSyncBehaviorState.MaxOutboundSamples * 2; i++) { var peerAddress = new IPAddress(i * 2); bool used = state.AddTimeData(peerAddress, TimeSpan.FromSeconds(800), false); Assert.True(used, $"index: {i}"); } diff = dateTimeProvider.GetAdjustedTime() - dateTimeProvider.GetUtcNow(); Assert.True(Math.Abs(diff.TotalMilliseconds - 800 * 1000) < TimeEpsilonMs, $"should be 800ms because of outbound preference. Actual: {diff.TotalMilliseconds}ms"); }
public void AddTimeData_WithSmallSampleSet_TurnsWarningOnAndSwitchesSyncOff() { int offsetAboveWarningLevelSeconds = TimeSyncBehaviorState.TimeOffsetWarningThresholdSeconds + 1; int offsetAboveWarningLevelMs = offsetAboveWarningLevelSeconds * 1000; int offsetAbovSwitchOffLevel = BitcoinMain.BitcoinMaxTimeOffsetSeconds + 1; int offsetAbovSwitchOffLevelMs = offsetAbovSwitchOffLevel * 1000; // Samples to be inserted to the state. // Columns meanings: isUsed, isWarningOn, isSyncOff, timeOffsetSample, peerAddress var samples = new List <TestSample> { // First group of samples does not affect adjusted time, so difference should be ~0 ms. TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(offsetAboveWarningLevelSeconds), IPAddress.Parse("1.2.3.41")), TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(offsetAboveWarningLevelSeconds), IPAddress.Parse("1.2.3.42")), TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(offsetAboveWarningLevelSeconds), IPAddress.Parse("1.2.3.43")), // The next sample turns on the warning. TestSample.Outbound(true, true, false, offsetAboveWarningLevelMs, TimeSpan.FromSeconds(offsetAboveWarningLevelSeconds), IPAddress.Parse("1.2.3.44")), // It can't be turned off. TestSample.Outbound(true, true, false, offsetAboveWarningLevelMs, TimeSpan.FromSeconds(0), IPAddress.Parse("1.2.3.45")), TestSample.Outbound(true, true, false, offsetAboveWarningLevelMs, TimeSpan.FromSeconds(0), IPAddress.Parse("1.2.3.46")), // Add more samples (above switch off level, trying to switch it off). TestSample.Outbound(true, true, false, offsetAboveWarningLevelMs, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.3.47")), TestSample.Outbound(true, true, false, offsetAboveWarningLevelMs / 2, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.3.48")), TestSample.Outbound(true, true, false, 0, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.3.49")), TestSample.Outbound(true, true, false, 0, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.31.4")), TestSample.Outbound(true, true, false, 0, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.32.4")), TestSample.Outbound(true, true, false, -offsetAbovSwitchOffLevelMs / 2, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.33.4")), // Now the feature should be turned off. TestSample.Outbound(true, true, true, 0, TimeSpan.FromSeconds(-offsetAbovSwitchOffLevel), IPAddress.Parse("1.2.33.5")), // No more samples should be accepted now. TestSample.Outbound(false, true, true, 0, TimeSpan.FromSeconds(2), IPAddress.Parse("1.2.34.4")), TestSample.Outbound(false, true, true, 0, TimeSpan.FromSeconds(1), IPAddress.Parse("1.2.35.4")), }; var dateTimeProvider = new DateTimeProvider(); TimeSyncBehaviorState state = this.CreateTimeSyncBehaviorState(dateTimeProvider); for (int i = 0; i < samples.Count; i++) { bool used = state.AddTimeData(samples[i].PeerIpAddress, samples[i].InputTimeOffset, samples[i].IsInbound); Assert.Equal(samples[i].ExpectedIsUsed, used); Assert.Equal(samples[i].ExpectedIsWarningOn, state.IsSystemTimeOutOfSync); Assert.Equal(samples[i].ExpectedIsSyncOff, state.SwitchedOffLimitReached); Assert.Equal(samples[i].ExpectedIsSyncOff, state.SwitchedOff); TimeSpan diff = dateTimeProvider.GetAdjustedTime() - dateTimeProvider.GetUtcNow(); Assert.True(Math.Abs(diff.TotalMilliseconds - samples[i].ExpectedTimeOffsetMs) < TimeEpsilonMs, $"Failed in sample at index: {i}. Actual offset milliseconds: {diff.TotalMilliseconds}. Expected offset milliseconds: {samples[i].ExpectedTimeOffsetMs}"); } }
public void AddTimeData_WithSmallSampleSet_CalculatedCorrectly() { // Samples to be inserted to the state. var samples = new List <TestSample> { // Columns meanings: expectedIsUsed, expectedIsWarningOn, expectedIsTimeSyncOff, expectedTimeOffsetMs, timeOffsetSample, peerAddress // First group of samples does not affect adjusted time, so difference should be ~0 ms. TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(3.56), new IPAddress(1)), TestSample.Inbound(true, false, false, 0, TimeSpan.FromSeconds(13.123), new IPAddress(1)), TestSample.Outbound(false, false, false, 0, TimeSpan.FromSeconds(7.123), new IPAddress(1)), // IP address already used for outbound. TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(26.0), IPAddress.Parse("2000:0db8:85a3:1232:0000:8a2e:0370:7334")), TestSample.Outbound(false, false, false, 0, TimeSpan.FromSeconds(260), IPAddress.Parse("2000:0db8:85a3:1232:0000:8a2e:0370:7334")), // IP address already used for outbound. TestSample.Outbound(true, false, false, 0, TimeSpan.FromSeconds(-2126.0), new IPAddress(2)), TestSample.Inbound(true, false, false, 0, TimeSpan.FromSeconds(-391), new IPAddress(3)), // These samples will change adjusted time because next outbound is the 4th outbound and we are under the limits. TestSample.Outbound(true, false, false, 1280, TimeSpan.FromSeconds(-1), new IPAddress(4)), // 2 inbound, 4 outbound. 2/4 * 3 = 1.5 -> ceil -> 2 of each outbound { -2126000, -2126000, -391000, -1000, -1000, 3560, 3560, 13123, 26000, 26000 } -> median is 1280ms. TestSample.Inbound(true, false, false, 3560, TimeSpan.FromSeconds(23.6), new IPAddress(5)), // 3 inbound, 4 outbound. 3/4 * 3 = 2.25 -> ceil -> 3 of each outbound { -2126000, -2126000, -2126000, -391000, -1000, -1000, -1000, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000 } -> median is 3560ms. TestSample.Inbound(true, false, false, 3560, TimeSpan.FromSeconds(236), new IPAddress(6)), // 4 inbound, 4 outbound. 4/4 * 3 = 3 -> ceil -> 3 of each outbound { -2126000, -2126000, -2126000, -391000, -1000, -1000, -1000, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000, 236000 } -> median is 3560ms. TestSample.Outbound(true, false, false, 1236, TimeSpan.FromSeconds(1.236), new IPAddress(7)), // 4 inbound, 5 outbound. 4/5 * 3 = 2.4 -> ceil -> 3 of each outbound { -2126000, -2126000, -2126000, -391000, -1000, -1000, -1000, 1236, 1236, 1236, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000, 236000 } -> median is 1236ms. TestSample.Inbound(true, false, false, 1236, TimeSpan.FromSeconds(-1001), new IPAddress(8)), // 5 inbound, 5 outbound. 5/5 * 3 = 3 -> ceil -> 3 of each outbound { -2126000, -2126000, -2126000, -391000, -1001, -1000, -1000, -1000, 1236, 1236, 1236, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000, 236000 } -> median is 1236ms. TestSample.Inbound(true, false, false, 1236, TimeSpan.FromSeconds(-4.9236), new IPAddress(9)), // 6 inbound, 5 outbound. 6/5 * 3 = 3.6 -> ceil -> 4 of each outbound { -2126000, -2126000, -2126000, -2126000, -391000, -4923.6, -1001, -1000, -1000, -1000, -1000, 1236, 1236, 1236, 1236, 3560, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000, 26000, 236000 } -> median is 1236ms. TestSample.Outbound(true, false, false, 118, TimeSpan.FromSeconds(-4444.444), new IPAddress(10)), // 6 inbound, 6 outbound. 6/6 * 3 = 3 -> ceil -> 3 of each outbound { -4444444, -4444444, -4444444, -2126000, -2126000, -2126000, -391000, -4923.6, -1001, -1000, -1000, -1000, 1236, 1236, 1236, 3560, 3560, 3560, 13123, 23600, 26000, 26000, 26000, 236000 } -> median is 118ms. }; int maliciousOffset = TimeSyncBehaviorState.MaxTimeOffsetSeconds - 1; // Introduce 100 malicious inbound and show we are protected from malicious inbounds. for (int i = 11; i < 111; i++) { // Median always lands on one of the outbounds no matter how many malicious inbounds. samples.Add(TestSample.Inbound(true, false, false, 1236, TimeSpan.FromSeconds(maliciousOffset), new IPAddress(i))); } // Add 3 malicious outbound which is 3/9 (33.3%) which is equal to the 33.3% of outbounds and show protected. // Median always lands on one of the non malicious outbound. samples.Add(TestSample.Outbound(true, false, false, 3560, TimeSpan.FromSeconds(maliciousOffset), new IPAddress(11))); samples.Add(TestSample.Outbound(true, false, false, 26000, TimeSpan.FromSeconds(maliciousOffset), new IPAddress(12))); samples.Add(TestSample.Outbound(true, false, false, 26000, TimeSpan.FromSeconds(maliciousOffset), new IPAddress(13))); // Add a 4th malicious outbound which is 4/10 (40%) which is greater that 33.3% of outbounds. Show we are not protected -> the offset gets set to the malicious value. samples.Add(TestSample.Outbound(true, false, false, maliciousOffset * 1000, TimeSpan.FromSeconds(maliciousOffset), new IPAddress(14))); var dateTimeProvider = new DateTimeProvider(); TimeSyncBehaviorState state = this.CreateTimeSyncBehaviorState(dateTimeProvider); for (int i = 0; i < samples.Count; i++) { bool used = state.AddTimeData(samples[i].PeerIpAddress, samples[i].InputTimeOffset, samples[i].IsInbound); Assert.Equal(samples[i].ExpectedIsUsed, used); Assert.Equal(samples[i].ExpectedIsSyncOff, state.SwitchedOffLimitReached); Assert.Equal(samples[i].ExpectedIsSyncOff, state.SwitchedOff); DateTime adjustedTime = dateTimeProvider.GetAdjustedTime(); DateTime normalTime = dateTimeProvider.GetUtcNow(); TimeSpan diff = adjustedTime - normalTime; Assert.True(Math.Abs(diff.TotalMilliseconds - samples[i].ExpectedTimeOffsetMs) < TimeEpsilonMs, $"Failed in sample at index: {i}. Actual offset milliseconds: {diff.TotalMilliseconds}. Expected offset milliseconds: {samples[i].ExpectedTimeOffsetMs}"); } }