aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Makefile.am4
-rw-r--r--src/Makefile.test.include4
-rw-r--r--src/addrman_impl.h1
-rw-r--r--src/init.cpp2
-rw-r--r--src/net.cpp1
-rw-r--r--src/net.h2
-rw-r--r--src/net_processing.cpp27
-rw-r--r--src/net_processing.h12
-rw-r--r--src/node/timeoffsets.cpp69
-rw-r--r--src/node/timeoffsets.h39
-rw-r--r--src/qt/guiutil.cpp4
-rw-r--r--src/qt/guiutil.h4
-rw-r--r--src/qt/rpcconsole.cpp3
-rw-r--r--src/rpc/net.cpp8
-rw-r--r--src/test/denialofservice_tests.cpp2
-rw-r--r--src/test/fuzz/timedata.cpp31
-rw-r--r--src/test/fuzz/timeoffsets.cpp28
-rw-r--r--src/test/net_tests.cpp5
-rw-r--r--src/test/timedata_tests.cpp105
-rw-r--r--src/test/timeoffsets_tests.cpp69
-rw-r--r--src/timedata.cpp116
-rw-r--r--src/timedata.h82
-rw-r--r--src/warnings.cpp11
-rw-r--r--src/warnings.h3
-rwxr-xr-xtest/functional/feature_maxtipage.py4
25 files changed, 268 insertions, 368 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index c2e0c7b5b8..639aecf3b3 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -230,6 +230,7 @@ BITCOIN_CORE_H = \
node/peerman_args.h \
node/protocol_version.h \
node/psbt.h \
+ node/timeoffsets.h \
node/transaction.h \
node/txreconciliation.h \
node/utxo_snapshot.h \
@@ -280,7 +281,6 @@ BITCOIN_CORE_H = \
support/lockedpool.h \
sync.h \
threadsafety.h \
- timedata.h \
torcontrol.h \
txdb.h \
txmempool.h \
@@ -434,6 +434,7 @@ libbitcoin_node_a_SOURCES = \
node/minisketchwrapper.cpp \
node/peerman_args.cpp \
node/psbt.cpp \
+ node/timeoffsets.cpp \
node/transaction.cpp \
node/txreconciliation.cpp \
node/utxo_snapshot.cpp \
@@ -461,7 +462,6 @@ libbitcoin_node_a_SOURCES = \
rpc/txoutproof.cpp \
script/sigcache.cpp \
signet.cpp \
- timedata.cpp \
torcontrol.cpp \
txdb.cpp \
txmempool.cpp \
diff --git a/src/Makefile.test.include b/src/Makefile.test.include
index 942e0bf69b..cfd28b0a4d 100644
--- a/src/Makefile.test.include
+++ b/src/Makefile.test.include
@@ -153,7 +153,7 @@ BITCOIN_TESTS =\
test/streams_tests.cpp \
test/sync_tests.cpp \
test/system_tests.cpp \
- test/timedata_tests.cpp \
+ test/timeoffsets_tests.cpp \
test/torcontrol_tests.cpp \
test/transaction_tests.cpp \
test/translation_tests.cpp \
@@ -384,7 +384,7 @@ test_fuzz_fuzz_SOURCES = \
test/fuzz/string.cpp \
test/fuzz/strprintf.cpp \
test/fuzz/system.cpp \
- test/fuzz/timedata.cpp \
+ test/fuzz/timeoffsets.cpp \
test/fuzz/torcontrol.cpp \
test/fuzz/transaction.cpp \
test/fuzz/tx_in.cpp \
diff --git a/src/addrman_impl.h b/src/addrman_impl.h
index 867c894d01..dd7f7b318f 100644
--- a/src/addrman_impl.h
+++ b/src/addrman_impl.h
@@ -11,7 +11,6 @@
#include <protocol.h>
#include <serialize.h>
#include <sync.h>
-#include <timedata.h>
#include <uint256.h>
#include <util/time.h>
diff --git a/src/init.cpp b/src/init.cpp
index 47d649be3d..616ebd5d81 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -67,7 +67,6 @@
#include <scheduler.h>
#include <script/sigcache.h>
#include <sync.h>
-#include <timedata.h>
#include <torcontrol.h>
#include <txdb.h>
#include <txmempool.h>
@@ -523,7 +522,6 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-maxconnections=<n>", strprintf("Maintain at most <n> automatic connections to peers (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxreceivebuffer=<n>", strprintf("Maximum per-connection receive buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXRECEIVEBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxsendbuffer=<n>", strprintf("Maximum per-connection memory usage for the send buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
- argsman.AddArg("-maxtimeadjustment", strprintf("Maximum allowed median peer time offset adjustment. Local perspective of time may be influenced by outbound peers forward or backward by this amount (default: %u seconds).", DEFAULT_MAX_TIME_ADJUSTMENT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxuploadtarget=<n>", strprintf("Tries to keep outbound traffic under the given target per 24h. Limit does not apply to peers with 'download' permission or blocks created within past week. 0 = no limit (default: %s). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base", DEFAULT_MAX_UPLOAD_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
#if HAVE_SOCKADDR_UN
argsman.AddArg("-onion=<ip:port|path>", "Use separate SOCKS5 proxy to reach peers via Tor onion services, set -noonion to disable (default: -proxy). May be a local file path prefixed with 'unix:'.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
diff --git a/src/net.cpp b/src/net.cpp
index 4801f5c1f9..98f2f4bad0 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -615,7 +615,6 @@ void CNode::CopyStats(CNodeStats& stats)
X(m_last_tx_time);
X(m_last_block_time);
X(m_connected);
- X(nTimeOffset);
X(m_addr_name);
X(nVersion);
{
diff --git a/src/net.h b/src/net.h
index 46d9422695..f1911fa2ce 100644
--- a/src/net.h
+++ b/src/net.h
@@ -191,7 +191,6 @@ public:
std::chrono::seconds m_last_tx_time;
std::chrono::seconds m_last_block_time;
std::chrono::seconds m_connected;
- int64_t nTimeOffset;
std::string m_addr_name;
int nVersion;
std::string cleanSubVer;
@@ -703,7 +702,6 @@ public:
std::atomic<std::chrono::seconds> m_last_recv{0s};
//! Unix epoch time at peer connection
const std::chrono::seconds m_connected;
- std::atomic<int64_t> nTimeOffset{0};
// Address of this peer
const CAddress addr;
// Bind address of our side of the connection
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index da4f99fb99..bdbf077ab5 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -23,6 +23,7 @@
#include <netbase.h>
#include <netmessagemaker.h>
#include <node/blockstorage.h>
+#include <node/timeoffsets.h>
#include <node/txreconciliation.h>
#include <policy/fees.h>
#include <policy/policy.h>
@@ -34,7 +35,6 @@
#include <scheduler.h>
#include <streams.h>
#include <sync.h>
-#include <timedata.h>
#include <tinyformat.h>
#include <txmempool.h>
#include <txorphanage.h>
@@ -390,6 +390,10 @@ struct Peer {
/** Whether this peer wants invs or headers (when possible) for block announcements */
bool m_prefers_headers GUARDED_BY(NetEventsInterface::g_msgproc_mutex){false};
+ /** Time offset computed during the version handshake based on the
+ * timestamp the peer sent in the version message. */
+ std::atomic<std::chrono::seconds> m_time_offset{0s};
+
explicit Peer(NodeId id, ServiceFlags our_services)
: m_id{id}
, m_our_services{our_services}
@@ -513,7 +517,7 @@ public:
std::optional<std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) override
EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
- bool IgnoresIncomingTxs() override { return m_opts.ignore_incoming_txs; }
+ PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void RelayTransaction(const uint256& txid, const uint256& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);
void SetBestBlock(int height, std::chrono::seconds time) override
@@ -788,6 +792,8 @@ private:
/** Next time to check for stale tip */
std::chrono::seconds m_stale_tip_check_time GUARDED_BY(cs_main){0s};
+ TimeOffsets m_outbound_time_offsets;
+
const Options m_opts;
bool RejectIncomingTxs(const CNode& peer) const;
@@ -1864,10 +1870,19 @@ bool PeerManagerImpl::GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) c
stats.presync_height = peer->m_headers_sync->GetPresyncHeight();
}
}
+ stats.time_offset = peer->m_time_offset;
return true;
}
+PeerManagerInfo PeerManagerImpl::GetInfo() const
+{
+ return PeerManagerInfo{
+ .median_outbound_time_offset = m_outbound_time_offsets.Median(),
+ .ignores_incoming_txs = m_opts.ignore_incoming_txs,
+ };
+}
+
void PeerManagerImpl::AddToCompactExtraTransactions(const CTransactionRef& tx)
{
if (m_opts.max_extra_txs <= 0)
@@ -3861,12 +3876,12 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
peer->m_starting_height, addrMe.ToStringAddrPort(), fRelay, pfrom.GetId(),
remoteAddr, (mapped_as ? strprintf(", mapped_as=%d", mapped_as) : ""));
- int64_t nTimeOffset = nTime - GetTime();
- pfrom.nTimeOffset = nTimeOffset;
+ peer->m_time_offset = NodeSeconds{std::chrono::seconds{nTime}} - Now<NodeSeconds>();
if (!pfrom.IsInboundConn()) {
// Don't use timedata samples from inbound peers to make it
- // harder for others to tamper with our adjusted time.
- AddTimeData(pfrom.addr, nTimeOffset);
+ // harder for others to create false warnings about our clock being out of sync.
+ m_outbound_time_offsets.Add(peer->m_time_offset);
+ m_outbound_time_offsets.WarnIfOutOfSync();
}
// If the peer is old enough to have the old alert system, send it the final alert.
diff --git a/src/net_processing.h b/src/net_processing.h
index f8d7a8f511..85e399d948 100644
--- a/src/net_processing.h
+++ b/src/net_processing.h
@@ -9,6 +9,8 @@
#include <net.h>
#include <validationinterface.h>
+#include <chrono>
+
class AddrMan;
class CChainParams;
class CTxMemPool;
@@ -41,6 +43,12 @@ struct CNodeStateStats {
bool m_addr_relay_enabled{false};
ServiceFlags their_services;
int64_t presync_height{-1};
+ std::chrono::seconds time_offset{0};
+};
+
+struct PeerManagerInfo {
+ std::chrono::seconds median_outbound_time_offset{0s};
+ bool ignores_incoming_txs{false};
};
class PeerManager : public CValidationInterface, public NetEventsInterface
@@ -83,8 +91,8 @@ public:
/** Get statistics from node state */
virtual bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const = 0;
- /** Whether this node ignores txs received over p2p. */
- virtual bool IgnoresIncomingTxs() = 0;
+ /** Get peer manager info. */
+ virtual PeerManagerInfo GetInfo() const = 0;
/** Relay transaction to all peers. */
virtual void RelayTransaction(const uint256& txid, const uint256& wtxid) = 0;
diff --git a/src/node/timeoffsets.cpp b/src/node/timeoffsets.cpp
new file mode 100644
index 0000000000..62f527be8a
--- /dev/null
+++ b/src/node/timeoffsets.cpp
@@ -0,0 +1,69 @@
+// Copyright (c) 2024-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <logging.h>
+#include <node/interface_ui.h>
+#include <node/timeoffsets.h>
+#include <sync.h>
+#include <tinyformat.h>
+#include <util/time.h>
+#include <util/translation.h>
+#include <warnings.h>
+
+#include <algorithm>
+#include <chrono>
+#include <cstdint>
+#include <deque>
+#include <limits>
+#include <optional>
+
+using namespace std::chrono_literals;
+
+void TimeOffsets::Add(std::chrono::seconds offset)
+{
+ LOCK(m_mutex);
+
+ if (m_offsets.size() >= MAX_SIZE) {
+ m_offsets.pop_front();
+ }
+ m_offsets.push_back(offset);
+ LogDebug(BCLog::NET, "Added time offset %+ds, total samples %d\n",
+ Ticks<std::chrono::seconds>(offset), m_offsets.size());
+}
+
+std::chrono::seconds TimeOffsets::Median() const
+{
+ LOCK(m_mutex);
+
+ // Only calculate the median if we have 5 or more offsets
+ if (m_offsets.size() < 5) return 0s;
+
+ auto sorted_copy = m_offsets;
+ std::sort(sorted_copy.begin(), sorted_copy.end());
+ return sorted_copy[sorted_copy.size() / 2]; // approximate median is good enough, keep it simple
+}
+
+bool TimeOffsets::WarnIfOutOfSync() const
+{
+ // when median == std::numeric_limits<int64_t>::min(), calling std::chrono::abs is UB
+ auto median{std::max(Median(), std::chrono::seconds(std::numeric_limits<int64_t>::min() + 1))};
+ if (std::chrono::abs(median) <= WARN_THRESHOLD) {
+ SetMedianTimeOffsetWarning(std::nullopt);
+ uiInterface.NotifyAlertChanged();
+ return false;
+ }
+
+ bilingual_str msg{strprintf(_(
+ "Your computer's date and time appear to be more than %d minutes out of sync with the network, "
+ "this may lead to consensus failure. After you've confirmed your computer's clock, this message "
+ "should no longer appear when you restart your node. Without a restart, it should stop showing "
+ "automatically after you've connected to a sufficient number of new outbound peers, which may "
+ "take some time. You can inspect the `timeoffset` field of the `getpeerinfo` and `getnetworkinfo` "
+ "RPC methods to get more info."
+ ), Ticks<std::chrono::minutes>(WARN_THRESHOLD))};
+ LogWarning("%s\n", msg.original);
+ SetMedianTimeOffsetWarning(msg);
+ uiInterface.NotifyAlertChanged();
+ return true;
+}
diff --git a/src/node/timeoffsets.h b/src/node/timeoffsets.h
new file mode 100644
index 0000000000..2b12584e12
--- /dev/null
+++ b/src/node/timeoffsets.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2024-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_NODE_TIMEOFFSETS_H
+#define BITCOIN_NODE_TIMEOFFSETS_H
+
+#include <sync.h>
+
+#include <chrono>
+#include <cstddef>
+#include <deque>
+
+class TimeOffsets
+{
+ //! Maximum number of timeoffsets stored.
+ static constexpr size_t MAX_SIZE{50};
+ //! Minimum difference between system and network time for a warning to be raised.
+ static constexpr std::chrono::minutes WARN_THRESHOLD{10};
+
+ mutable Mutex m_mutex;
+ /** The observed time differences between our local clock and those of our outbound peers. A
+ * positive offset means our peer's clock is ahead of our local clock. */
+ std::deque<std::chrono::seconds> m_offsets GUARDED_BY(m_mutex){};
+
+public:
+ /** Add a new time offset sample. */
+ void Add(std::chrono::seconds offset) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+
+ /** Compute and return the median of the collected time offset samples. The median is returned
+ * as 0 when there are less than 5 samples. */
+ std::chrono::seconds Median() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+
+ /** Raise warnings if the median time offset exceeds the warnings threshold. Returns true if
+ * warnings were raised. */
+ bool WarnIfOutOfSync() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);
+};
+
+#endif // BITCOIN_NODE_TIMEOFFSETS_H
diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp
index 1d1a84e375..e094b686d4 100644
--- a/src/qt/guiutil.cpp
+++ b/src/qt/guiutil.cpp
@@ -775,9 +775,9 @@ QString formatPingTime(std::chrono::microseconds ping_time)
QObject::tr("%1 ms").arg(QString::number((int)(count_microseconds(ping_time) / 1000), 10));
}
-QString formatTimeOffset(int64_t nTimeOffset)
+QString formatTimeOffset(int64_t time_offset)
{
- return QObject::tr("%1 s").arg(QString::number((int)nTimeOffset, 10));
+ return QObject::tr("%1 s").arg(QString::number((int)time_offset, 10));
}
QString formatNiceTimeOffset(qint64 secs)
diff --git a/src/qt/guiutil.h b/src/qt/guiutil.h
index 7f06fdfe37..3e28e54557 100644
--- a/src/qt/guiutil.h
+++ b/src/qt/guiutil.h
@@ -240,8 +240,8 @@ namespace GUIUtil
/** Format a CNodeStats.m_last_ping_time into a user-readable string or display N/A, if 0 */
QString formatPingTime(std::chrono::microseconds ping_time);
- /** Format a CNodeCombinedStats.nTimeOffset into a user-readable string */
- QString formatTimeOffset(int64_t nTimeOffset);
+ /** Format a CNodeStateStats.time_offset into a user-readable string */
+ QString formatTimeOffset(int64_t time_offset);
QString formatNiceTimeOffset(qint64 secs);
diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp
index d2b184ebdf..bafef62945 100644
--- a/src/qt/rpcconsole.cpp
+++ b/src/qt/rpcconsole.cpp
@@ -23,6 +23,7 @@
#include <rpc/server.h>
#include <util/strencodings.h>
#include <util/string.h>
+#include <util/time.h>
#include <util/threadnames.h>
#include <univalue.h>
@@ -1196,7 +1197,6 @@ void RPCConsole::updateDetailWidget()
ui->peerBytesRecv->setText(GUIUtil::formatBytes(stats->nodeStats.nRecvBytes));
ui->peerPingTime->setText(GUIUtil::formatPingTime(stats->nodeStats.m_last_ping_time));
ui->peerMinPing->setText(GUIUtil::formatPingTime(stats->nodeStats.m_min_ping_time));
- ui->timeoffset->setText(GUIUtil::formatTimeOffset(stats->nodeStats.nTimeOffset));
if (stats->nodeStats.nVersion) {
ui->peerVersion->setText(QString::number(stats->nodeStats.nVersion));
}
@@ -1228,6 +1228,7 @@ void RPCConsole::updateDetailWidget()
// This check fails for example if the lock was busy and
// nodeStateStats couldn't be fetched.
if (stats->fNodeStateStatsAvailable) {
+ ui->timeoffset->setText(GUIUtil::formatTimeOffset(Ticks<std::chrono::seconds>(stats->nodeStateStats.time_offset)));
ui->peerServices->setText(GUIUtil::formatServicesStr(stats->nodeStateStats.their_services));
// Sync height is init to -1
if (stats->nodeStateStats.nSyncHeight > -1) {
diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp
index 49789f538b..739ff1a48e 100644
--- a/src/rpc/net.cpp
+++ b/src/rpc/net.cpp
@@ -23,7 +23,6 @@
#include <rpc/server_util.h>
#include <rpc/util.h>
#include <sync.h>
-#include <timedata.h>
#include <util/chaintype.h>
#include <util/strencodings.h>
#include <util/string.h>
@@ -239,7 +238,7 @@ static RPCHelpMan getpeerinfo()
obj.pushKV("bytessent", stats.nSendBytes);
obj.pushKV("bytesrecv", stats.nRecvBytes);
obj.pushKV("conntime", count_seconds(stats.m_connected));
- obj.pushKV("timeoffset", stats.nTimeOffset);
+ obj.pushKV("timeoffset", Ticks<std::chrono::seconds>(statestats.time_offset));
if (stats.m_last_ping_time > 0us) {
obj.pushKV("pingtime", Ticks<SecondsDouble>(stats.m_last_ping_time));
}
@@ -679,9 +678,10 @@ static RPCHelpMan getnetworkinfo()
obj.pushKV("localservicesnames", GetServicesNames(services));
}
if (node.peerman) {
- obj.pushKV("localrelay", !node.peerman->IgnoresIncomingTxs());
+ auto peerman_info{node.peerman->GetInfo()};
+ obj.pushKV("localrelay", !peerman_info.ignores_incoming_txs);
+ obj.pushKV("timeoffset", Ticks<std::chrono::seconds>(peerman_info.median_outbound_time_offset));
}
- obj.pushKV("timeoffset", GetTimeOffset());
if (node.connman) {
obj.pushKV("networkactive", node.connman->GetNetworkActive());
obj.pushKV("connections", node.connman->GetNodeCount(ConnectionDirection::Both));
diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp
index 0fef8c5906..5e9ae78681 100644
--- a/src/test/denialofservice_tests.cpp
+++ b/src/test/denialofservice_tests.cpp
@@ -16,7 +16,6 @@
#include <test/util/net.h>
#include <test/util/random.h>
#include <test/util/setup_common.h>
-#include <timedata.h>
#include <util/string.h>
#include <util/time.h>
#include <validation.h>
@@ -72,7 +71,6 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
/*local_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS),
/*version=*/PROTOCOL_VERSION,
/*relay_txs=*/true);
- TestOnlyResetTimeData();
// This test requires that we have a chain with non-zero work.
{
diff --git a/src/test/fuzz/timedata.cpp b/src/test/fuzz/timedata.cpp
deleted file mode 100644
index f5d005296b..0000000000
--- a/src/test/fuzz/timedata.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2020-2021 The Bitcoin Core developers
-// Distributed under the MIT software license, see the accompanying
-// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-#include <test/fuzz/FuzzedDataProvider.h>
-#include <test/fuzz/fuzz.h>
-#include <test/fuzz/util.h>
-#include <timedata.h>
-
-#include <cstdint>
-#include <string>
-#include <vector>
-
-FUZZ_TARGET(timedata)
-{
- FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
- const unsigned int max_size = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 1000);
- // A max_size of 0 implies no limit, so cap the max number of insertions to avoid timeouts
- auto max_to_insert = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 4000);
- // Divide by 2 to avoid signed integer overflow in .median()
- const int64_t initial_value = fuzzed_data_provider.ConsumeIntegral<int64_t>() / 2;
- CMedianFilter<int64_t> median_filter{max_size, initial_value};
- while (fuzzed_data_provider.remaining_bytes() > 0 && --max_to_insert >= 0) {
- (void)median_filter.median();
- assert(median_filter.size() > 0);
- assert(static_cast<size_t>(median_filter.size()) == median_filter.sorted().size());
- assert(static_cast<unsigned int>(median_filter.size()) <= max_size || max_size == 0);
- // Divide by 2 to avoid signed integer overflow in .median()
- median_filter.input(fuzzed_data_provider.ConsumeIntegral<int64_t>() / 2);
- }
-}
diff --git a/src/test/fuzz/timeoffsets.cpp b/src/test/fuzz/timeoffsets.cpp
new file mode 100644
index 0000000000..019337a94a
--- /dev/null
+++ b/src/test/fuzz/timeoffsets.cpp
@@ -0,0 +1,28 @@
+// Copyright (c) 2024-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <node/timeoffsets.h>
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+#include <test/util/setup_common.h>
+
+#include <chrono>
+#include <cstdint>
+#include <functional>
+
+void initialize_timeoffsets()
+{
+ static const auto testing_setup = MakeNoLogFileContext<>(ChainType::MAIN);
+}
+
+FUZZ_TARGET(timeoffsets, .init = initialize_timeoffsets)
+{
+ FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+ TimeOffsets offsets{};
+ LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 4'000) {
+ (void)offsets.Median();
+ offsets.Add(std::chrono::seconds{fuzzed_data_provider.ConsumeIntegral<std::chrono::seconds::rep>()});
+ offsets.WarnIfOutOfSync();
+ }
+}
diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp
index 70a8279f04..b9dff96610 100644
--- a/src/test/net_tests.cpp
+++ b/src/test/net_tests.cpp
@@ -19,7 +19,6 @@
#include <test/util/random.h>
#include <test/util/setup_common.h>
#include <test/util/validation.h>
-#include <timedata.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <validation.h>
@@ -902,10 +901,6 @@ BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message)
chainman.ResetIbd();
m_node.args->ForceSetArg("-capturemessages", "0");
m_node.args->ForceSetArg("-bind", "");
- // PeerManager::ProcessMessage() calls AddTimeData() which changes the internal state
- // in timedata.cpp and later confuses the test "timedata_tests/addtimedata". Thus reset
- // that state as it was before our test was run.
- TestOnlyResetTimeData();
}
diff --git a/src/test/timedata_tests.cpp b/src/test/timedata_tests.cpp
deleted file mode 100644
index 2814dbf4c0..0000000000
--- a/src/test/timedata_tests.cpp
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (c) 2011-2021 The Bitcoin Core developers
-// Distributed under the MIT software license, see the accompanying
-// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-//
-
-#include <netaddress.h>
-#include <noui.h>
-#include <test/util/logging.h>
-#include <test/util/setup_common.h>
-#include <timedata.h>
-#include <util/string.h>
-#include <util/translation.h>
-#include <warnings.h>
-
-#include <string>
-
-#include <boost/test/unit_test.hpp>
-
-BOOST_FIXTURE_TEST_SUITE(timedata_tests, BasicTestingSetup)
-
-BOOST_AUTO_TEST_CASE(util_MedianFilter)
-{
- CMedianFilter<int> filter(5, 15);
-
- BOOST_CHECK_EQUAL(filter.median(), 15);
-
- filter.input(20); // [15 20]
- BOOST_CHECK_EQUAL(filter.median(), 17);
-
- filter.input(30); // [15 20 30]
- BOOST_CHECK_EQUAL(filter.median(), 20);
-
- filter.input(3); // [3 15 20 30]
- BOOST_CHECK_EQUAL(filter.median(), 17);
-
- filter.input(7); // [3 7 15 20 30]
- BOOST_CHECK_EQUAL(filter.median(), 15);
-
- filter.input(18); // [3 7 18 20 30]
- BOOST_CHECK_EQUAL(filter.median(), 18);
-
- filter.input(0); // [0 3 7 18 30]
- BOOST_CHECK_EQUAL(filter.median(), 7);
-}
-
-static void MultiAddTimeData(int n, int64_t offset)
-{
- static int cnt = 0;
- for (int i = 0; i < n; ++i) {
- CNetAddr addr;
- addr.SetInternal(ToString(++cnt));
- AddTimeData(addr, offset);
- }
-}
-
-
-BOOST_AUTO_TEST_CASE(addtimedata)
-{
- BOOST_CHECK_EQUAL(GetTimeOffset(), 0);
-
- //Part 1: Add large offsets to test a warning message that our clock may be wrong.
- MultiAddTimeData(3, DEFAULT_MAX_TIME_ADJUSTMENT + 1);
- // Filter size is 1 + 3 = 4: It is always initialized with a single element (offset 0)
-
- {
- ASSERT_DEBUG_LOG("Please check that your computer's date and time are correct!");
- MultiAddTimeData(1, DEFAULT_MAX_TIME_ADJUSTMENT + 1); //filter size 5
- }
-
- BOOST_CHECK(GetWarnings(true).original.find("clock is wrong") != std::string::npos);
-
- // nTimeOffset is not changed if the median of offsets exceeds DEFAULT_MAX_TIME_ADJUSTMENT
- BOOST_CHECK_EQUAL(GetTimeOffset(), 0);
-
- // Part 2: Test positive and negative medians by adding more offsets
- MultiAddTimeData(4, 100); // filter size 9
- BOOST_CHECK_EQUAL(GetTimeOffset(), 100);
- MultiAddTimeData(10, -100); //filter size 19
- BOOST_CHECK_EQUAL(GetTimeOffset(), -100);
-
- // Part 3: Test behaviour when filter has reached maximum number of offsets
- const int MAX_SAMPLES = 200;
- int nfill = (MAX_SAMPLES - 3 - 19) / 2; //89
- MultiAddTimeData(nfill, 100);
- MultiAddTimeData(nfill, -100); //filter size MAX_SAMPLES - 3
- BOOST_CHECK_EQUAL(GetTimeOffset(), -100);
-
- MultiAddTimeData(2, 100);
- //filter size MAX_SAMPLES -1, median is the initial 0 offset
- //since we added same number of positive/negative offsets
-
- BOOST_CHECK_EQUAL(GetTimeOffset(), 0);
-
- // After the number of offsets has reached MAX_SAMPLES -1 (=199), nTimeOffset will never change
- // because it is only updated when the number of elements in the filter becomes odd. It was decided
- // not to fix this because it prevents possible attacks. See the comment in AddTimeData() or issue #4521
- // for a more detailed explanation.
- MultiAddTimeData(2, 100); // filter median is 100 now, but nTimeOffset will not change
- // We want this test to end with nTimeOffset==0, otherwise subsequent tests of the suite will fail.
- BOOST_CHECK_EQUAL(GetTimeOffset(), 0);
-
- TestOnlyResetTimeData();
-}
-
-BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/timeoffsets_tests.cpp b/src/test/timeoffsets_tests.cpp
new file mode 100644
index 0000000000..008f1a9db9
--- /dev/null
+++ b/src/test/timeoffsets_tests.cpp
@@ -0,0 +1,69 @@
+// Copyright (c) 2024-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+//
+
+#include <node/timeoffsets.h>
+#include <test/util/setup_common.h>
+
+#include <boost/test/unit_test.hpp>
+
+#include <chrono>
+#include <vector>
+
+using namespace std::chrono_literals;
+
+static void AddMulti(TimeOffsets& offsets, const std::vector<std::chrono::seconds>& to_add)
+{
+ for (auto offset : to_add) {
+ offsets.Add(offset);
+ }
+}
+
+BOOST_FIXTURE_TEST_SUITE(timeoffsets_tests, BasicTestingSetup)
+
+BOOST_AUTO_TEST_CASE(timeoffsets)
+{
+ TimeOffsets offsets{};
+ BOOST_CHECK(offsets.Median() == 0s);
+
+ AddMulti(offsets, {{0s, -1s, -2s, -3s}});
+ // median should be zero for < 5 offsets
+ BOOST_CHECK(offsets.Median() == 0s);
+
+ offsets.Add(-4s);
+ // we now have 5 offsets: [-4, -3, -2, -1, 0]
+ BOOST_CHECK(offsets.Median() == -2s);
+
+ AddMulti(offsets, {4, 5s});
+ // we now have 9 offsets: [-4, -3, -2, -1, 0, 5, 5, 5, 5]
+ BOOST_CHECK(offsets.Median() == 0s);
+
+ AddMulti(offsets, {41, 10s});
+ // the TimeOffsets is now at capacity with 50 offsets, oldest offsets is discarded for any additional offset
+ BOOST_CHECK(offsets.Median() == 10s);
+
+ AddMulti(offsets, {25, 15s});
+ // we now have 25 offsets of 10s followed by 25 offsets of 15s
+ BOOST_CHECK(offsets.Median() == 15s);
+}
+
+static bool IsWarningRaised(const std::vector<std::chrono::seconds>& check_offsets)
+{
+ TimeOffsets offsets{};
+ AddMulti(offsets, check_offsets);
+ return offsets.WarnIfOutOfSync();
+}
+
+
+BOOST_AUTO_TEST_CASE(timeoffsets_warning)
+{
+ BOOST_CHECK(IsWarningRaised({{-60min, -40min, -30min, 0min, 10min}}));
+ BOOST_CHECK(IsWarningRaised({5, 11min}));
+
+ BOOST_CHECK(!IsWarningRaised({4, 60min}));
+ BOOST_CHECK(!IsWarningRaised({100, 3min}));
+}
+
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/timedata.cpp b/src/timedata.cpp
deleted file mode 100644
index d948de976f..0000000000
--- a/src/timedata.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright (c) 2014-2022 The Bitcoin Core developers
-// Distributed under the MIT software license, see the accompanying
-// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-#if defined(HAVE_CONFIG_H)
-#include <config/bitcoin-config.h>
-#endif
-
-#include <timedata.h>
-
-#include <common/args.h>
-#include <logging.h>
-#include <netaddress.h>
-#include <node/interface_ui.h>
-#include <sync.h>
-#include <tinyformat.h>
-#include <util/translation.h>
-#include <warnings.h>
-
-static GlobalMutex g_timeoffset_mutex;
-static int64_t nTimeOffset GUARDED_BY(g_timeoffset_mutex) = 0;
-
-/**
- * "Never go to sea with two chronometers; take one or three."
- * Our three time sources are:
- * - System clock
- * - Median of other nodes clocks
- * - The user (asking the user to fix the system clock if the first two disagree)
- */
-int64_t GetTimeOffset()
-{
- LOCK(g_timeoffset_mutex);
- return nTimeOffset;
-}
-
-#define BITCOIN_TIMEDATA_MAX_SAMPLES 200
-
-static std::set<CNetAddr> g_sources;
-static CMedianFilter<int64_t> g_time_offsets{BITCOIN_TIMEDATA_MAX_SAMPLES, 0};
-static bool g_warning_emitted;
-
-void AddTimeData(const CNetAddr& ip, int64_t nOffsetSample)
-{
- LOCK(g_timeoffset_mutex);
- // Ignore duplicates
- if (g_sources.size() == BITCOIN_TIMEDATA_MAX_SAMPLES)
- return;
- if (!g_sources.insert(ip).second)
- return;
-
- // Add data
- g_time_offsets.input(nOffsetSample);
- LogPrint(BCLog::NET, "added time data, samples %d, offset %+d (%+d minutes)\n", g_time_offsets.size(), nOffsetSample, nOffsetSample / 60);
-
- // There is a known issue here (see issue #4521):
- //
- // - The structure g_time_offsets contains up to 200 elements, after which
- // any new element added to it will not increase its size, replacing the
- // oldest element.
- //
- // - The condition to update nTimeOffset includes checking whether the
- // number of elements in g_time_offsets is odd, which will never happen after
- // there are 200 elements.
- //
- // But in this case the 'bug' is protective against some attacks, and may
- // actually explain why we've never seen attacks which manipulate the
- // clock offset.
- //
- // So we should hold off on fixing this and clean it up as part of
- // a timing cleanup that strengthens it in a number of other ways.
- //
- if (g_time_offsets.size() >= 5 && g_time_offsets.size() % 2 == 1) {
- int64_t nMedian = g_time_offsets.median();
- std::vector<int64_t> vSorted = g_time_offsets.sorted();
- // Only let other nodes change our time by so much
- int64_t max_adjustment = std::max<int64_t>(0, gArgs.GetIntArg("-maxtimeadjustment", DEFAULT_MAX_TIME_ADJUSTMENT));
- if (nMedian >= -max_adjustment && nMedian <= max_adjustment) {
- nTimeOffset = nMedian;
- } else {
- nTimeOffset = 0;
-
- if (!g_warning_emitted) {
- // If nobody has a time different than ours but within 5 minutes of ours, give a warning
- bool fMatch = false;
- for (const int64_t nOffset : vSorted) {
- if (nOffset != 0 && nOffset > -5 * 60 && nOffset < 5 * 60) fMatch = true;
- }
-
- if (!fMatch) {
- g_warning_emitted = true;
- bilingual_str strMessage = strprintf(_("Please check that your computer's date and time are correct! If your clock is wrong, %s will not work properly."), PACKAGE_NAME);
- SetMiscWarning(strMessage);
- uiInterface.ThreadSafeMessageBox(strMessage, "", CClientUIInterface::MSG_WARNING);
- }
- }
- }
-
- if (LogAcceptCategory(BCLog::NET, BCLog::Level::Debug)) {
- std::string log_message{"time data samples: "};
- for (const int64_t n : vSorted) {
- log_message += strprintf("%+d ", n);
- }
- log_message += strprintf("| median offset = %+d (%+d minutes)", nTimeOffset, nTimeOffset / 60);
- LogPrint(BCLog::NET, "%s\n", log_message);
- }
- }
-}
-
-void TestOnlyResetTimeData()
-{
- LOCK(g_timeoffset_mutex);
- nTimeOffset = 0;
- g_sources.clear();
- g_time_offsets = CMedianFilter<int64_t>{BITCOIN_TIMEDATA_MAX_SAMPLES, 0};
- g_warning_emitted = false;
-}
diff --git a/src/timedata.h b/src/timedata.h
deleted file mode 100644
index 3e76f80452..0000000000
--- a/src/timedata.h
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) 2014-2022 The Bitcoin Core developers
-// Distributed under the MIT software license, see the accompanying
-// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-#ifndef BITCOIN_TIMEDATA_H
-#define BITCOIN_TIMEDATA_H
-
-#include <algorithm>
-#include <cassert>
-#include <cstdint>
-#include <vector>
-
-static const int64_t DEFAULT_MAX_TIME_ADJUSTMENT = 70 * 60;
-
-class CNetAddr;
-
-/**
- * Median filter over a stream of values.
- * Returns the median of the last N numbers
- */
-template <typename T>
-class CMedianFilter
-{
-private:
- std::vector<T> vValues;
- std::vector<T> vSorted;
- unsigned int nSize;
-
-public:
- CMedianFilter(unsigned int _size, T initial_value) : nSize(_size)
- {
- vValues.reserve(_size);
- vValues.push_back(initial_value);
- vSorted = vValues;
- }
-
- void input(T value)
- {
- if (vValues.size() == nSize) {
- vValues.erase(vValues.begin());
- }
- vValues.push_back(value);
-
- vSorted.resize(vValues.size());
- std::copy(vValues.begin(), vValues.end(), vSorted.begin());
- std::sort(vSorted.begin(), vSorted.end());
- }
-
- T median() const
- {
- int vSortedSize = vSorted.size();
- assert(vSortedSize > 0);
- if (vSortedSize & 1) // Odd number of elements
- {
- return vSorted[vSortedSize / 2];
- } else // Even number of elements
- {
- return (vSorted[vSortedSize / 2 - 1] + vSorted[vSortedSize / 2]) / 2;
- }
- }
-
- int size() const
- {
- return vValues.size();
- }
-
- std::vector<T> sorted() const
- {
- return vSorted;
- }
-};
-
-/** Functions to keep track of adjusted P2P time */
-int64_t GetTimeOffset();
-void AddTimeData(const CNetAddr& ip, int64_t nTime);
-
-/**
- * Reset the internal state of GetTimeOffset() and AddTimeData().
- */
-void TestOnlyResetTimeData();
-
-#endif // BITCOIN_TIMEDATA_H
diff --git a/src/warnings.cpp b/src/warnings.cpp
index 84b021dad5..d55eecc48d 100644
--- a/src/warnings.cpp
+++ b/src/warnings.cpp
@@ -14,11 +14,13 @@
#include <util/string.h>
#include <util/translation.h>
+#include <optional>
#include <vector>
static GlobalMutex g_warnings_mutex;
static bilingual_str g_misc_warnings GUARDED_BY(g_warnings_mutex);
static bool fLargeWorkInvalidChainFound GUARDED_BY(g_warnings_mutex) = false;
+static std::optional<bilingual_str> g_timeoffset_warning GUARDED_BY(g_warnings_mutex){};
void SetMiscWarning(const bilingual_str& warning)
{
@@ -32,6 +34,11 @@ void SetfLargeWorkInvalidChainFound(bool flag)
fLargeWorkInvalidChainFound = flag;
}
+void SetMedianTimeOffsetWarning(std::optional<bilingual_str> warning)
+{
+ LOCK(g_warnings_mutex);
+ g_timeoffset_warning = warning;
+}
bilingual_str GetWarnings(bool verbose)
{
bilingual_str warnings_concise;
@@ -56,6 +63,10 @@ bilingual_str GetWarnings(bool verbose)
warnings_verbose.emplace_back(warnings_concise);
}
+ if (g_timeoffset_warning) {
+ warnings_verbose.emplace_back(g_timeoffset_warning.value());
+ }
+
if (verbose) {
return Join(warnings_verbose, Untranslated("<hr />"));
}
diff --git a/src/warnings.h b/src/warnings.h
index b21e2ea2b8..866bce6246 100644
--- a/src/warnings.h
+++ b/src/warnings.h
@@ -6,12 +6,15 @@
#ifndef BITCOIN_WARNINGS_H
#define BITCOIN_WARNINGS_H
+#include <optional>
#include <string>
struct bilingual_str;
void SetMiscWarning(const bilingual_str& warning);
void SetfLargeWorkInvalidChainFound(bool flag);
+/** Pass std::nullopt to disable the warning */
+void SetMedianTimeOffsetWarning(std::optional<bilingual_str> warning);
/** Format a string that describes several potential problems detected by the core.
* @param[in] verbose bool
* - if true, get all warnings separated by <hr />
diff --git a/test/functional/feature_maxtipage.py b/test/functional/feature_maxtipage.py
index 51f37ef1e0..a1774a5395 100755
--- a/test/functional/feature_maxtipage.py
+++ b/test/functional/feature_maxtipage.py
@@ -43,6 +43,10 @@ class MaxTipAgeTest(BitcoinTestFramework):
self.generate(node_miner, 1)
assert_equal(node_ibd.getblockchaininfo()['initialblockdownload'], False)
+ # reset time to system time so we don't have a time offset with the ibd node the next
+ # time we connect to it, ensuring TimeOffsets::WarnIfOutOfSync() doesn't output to stderr
+ node_miner.setmocktime(0)
+
def run_test(self):
self.log.info("Test IBD with maximum tip age of 24 hours (default).")
self.test_maxtipage(DEFAULT_MAX_TIP_AGE, set_parameter=False)