Skip to content

Commit 107b855

Browse files
author
MarcoFalke
committed
Merge #18638: net: Use mockable time for ping/pong, add tests
fa33654 net: Use mockable time for ping/pong, add tests (MarcoFalke) faab4aa util: Add count_microseconds helper (MarcoFalke) Pull request description: Switch `CNode::m_ping_start` and `CNetMessage::m_time` to mockable time, so that tests can be added. Mockable time is also type-safe, since it uses `std::chrono` ACKs for top commit: jonatack: Code review re-ACK fa33654 re-read code, verified rebase per `git range-diff 4b5c919 fa94d6f fa33654`, previous tested ACKs still valid troygiorshev: ACK fa33654 Tree-SHA512: 7d632bd6019ce7c882029e71b667a61517e783af82755a85dd979ef09380934e172dec8b8f91d57b200a30a6e096aeaf01f19fee7f3aed0e0e871c72eb44d70e
2 parents a4eb6a5 + fa33654 commit 107b855

File tree

10 files changed

+173
-36
lines changed

10 files changed

+173
-36
lines changed

src/net.cpp

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -564,15 +564,15 @@ void CNode::copyStats(CNodeStats &stats, const std::vector<bool> &m_asmap)
564564
// since pingtime does not update until the ping is complete, which might take a while.
565565
// So, if a ping is taking an unusually long time in flight,
566566
// the caller can immediately detect that this is happening.
567-
int64_t nPingUsecWait = 0;
568-
if ((0 != nPingNonceSent) && (0 != nPingUsecStart)) {
569-
nPingUsecWait = GetTimeMicros() - nPingUsecStart;
567+
std::chrono::microseconds ping_wait{0};
568+
if ((0 != nPingNonceSent) && (0 != m_ping_start.load().count())) {
569+
ping_wait = GetTime<std::chrono::microseconds>() - m_ping_start.load();
570570
}
571571

572572
// Raw ping time is in microseconds, but show it to user as whole seconds (Bitcoin users should be well used to small numbers with many decimal places by now :)
573573
stats.m_ping_usec = nPingUsecTime;
574574
stats.m_min_ping_usec = nMinPingUsecTime;
575-
stats.m_ping_wait_usec = nPingUsecWait;
575+
stats.m_ping_wait_usec = count_microseconds(ping_wait);
576576

577577
// Leave string empty if addrLocal invalid (not filled in yet)
578578
CService addrLocalUnlocked = GetAddrLocal();
@@ -583,9 +583,9 @@ void CNode::copyStats(CNodeStats &stats, const std::vector<bool> &m_asmap)
583583
bool CNode::ReceiveMsgBytes(const char *pch, unsigned int nBytes, bool& complete)
584584
{
585585
complete = false;
586-
int64_t nTimeMicros = GetTimeMicros();
586+
const auto time = GetTime<std::chrono::microseconds>();
587587
LOCK(cs_vRecv);
588-
nLastRecv = nTimeMicros / 1000000;
588+
nLastRecv = std::chrono::duration_cast<std::chrono::seconds>(time).count();
589589
nRecvBytes += nBytes;
590590
while (nBytes > 0) {
591591
// absorb network data
@@ -597,7 +597,7 @@ bool CNode::ReceiveMsgBytes(const char *pch, unsigned int nBytes, bool& complete
597597

598598
if (m_deserializer->Complete()) {
599599
// decompose a transport agnostic CNetMessage from the deserializer
600-
CNetMessage msg = m_deserializer->GetMessage(Params().MessageStart(), nTimeMicros);
600+
CNetMessage msg = m_deserializer->GetMessage(Params().MessageStart(), time);
601601

602602
//store received bytes per message command
603603
//to prevent a memory DOS, only allow valid commands
@@ -700,7 +700,8 @@ const uint256& V1TransportDeserializer::GetMessageHash() const
700700
return data_hash;
701701
}
702702

703-
CNetMessage V1TransportDeserializer::GetMessage(const CMessageHeader::MessageStartChars& message_start, int64_t time) {
703+
CNetMessage V1TransportDeserializer::GetMessage(const CMessageHeader::MessageStartChars& message_start, const std::chrono::microseconds time)
704+
{
704705
// decompose a single CNetMessage from the TransportDeserializer
705706
CNetMessage msg(std::move(vRecv));
706707

@@ -1159,9 +1160,9 @@ void CConnman::InactivityCheck(CNode *pnode)
11591160
LogPrintf("socket receive timeout: %is\n", nTime - pnode->nLastRecv);
11601161
pnode->fDisconnect = true;
11611162
}
1162-
else if (pnode->nPingNonceSent && pnode->nPingUsecStart + TIMEOUT_INTERVAL * 1000000 < GetTimeMicros())
1163+
else if (pnode->nPingNonceSent && pnode->m_ping_start.load() + std::chrono::seconds{TIMEOUT_INTERVAL} < GetTime<std::chrono::microseconds>())
11631164
{
1164-
LogPrintf("ping timeout: %fs\n", 0.000001 * (GetTimeMicros() - pnode->nPingUsecStart));
1165+
LogPrintf("ping timeout: %fs\n", 0.000001 * count_microseconds(GetTime<std::chrono::microseconds>() - pnode->m_ping_start.load()));
11651166
pnode->fDisconnect = true;
11661167
}
11671168
else if (!pnode->fSuccessfullyConnected)

src/net.h

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -612,13 +612,13 @@ class CNodeStats
612612
*/
613613
class CNetMessage {
614614
public:
615-
CDataStream m_recv; // received message data
616-
int64_t m_time = 0; // time (in microseconds) of message receipt.
615+
CDataStream m_recv; //!< received message data
616+
std::chrono::microseconds m_time{0}; //!< time of message receipt
617617
bool m_valid_netmagic = false;
618618
bool m_valid_header = false;
619619
bool m_valid_checksum = false;
620-
uint32_t m_message_size = 0; // size of the payload
621-
uint32_t m_raw_message_size = 0; // used wire size of the message (including header/checksum)
620+
uint32_t m_message_size{0}; //!< size of the payload
621+
uint32_t m_raw_message_size{0}; //!< used wire size of the message (including header/checksum)
622622
std::string m_command;
623623

624624
CNetMessage(CDataStream&& recv_in) : m_recv(std::move(recv_in)) {}
@@ -642,7 +642,7 @@ class TransportDeserializer {
642642
// read and deserialize data
643643
virtual int Read(const char *data, unsigned int bytes) = 0;
644644
// decomposes a message from the context
645-
virtual CNetMessage GetMessage(const CMessageHeader::MessageStartChars& message_start, int64_t time) = 0;
645+
virtual CNetMessage GetMessage(const CMessageHeader::MessageStartChars& message_start, std::chrono::microseconds time) = 0;
646646
virtual ~TransportDeserializer() {}
647647
};
648648

@@ -695,7 +695,7 @@ class V1TransportDeserializer final : public TransportDeserializer
695695
if (ret < 0) Reset();
696696
return ret;
697697
}
698-
CNetMessage GetMessage(const CMessageHeader::MessageStartChars& message_start, int64_t time) override;
698+
CNetMessage GetMessage(const CMessageHeader::MessageStartChars& message_start, std::chrono::microseconds time) override;
699699
};
700700

701701
/** The TransportSerializer prepares messages for the network transport
@@ -845,8 +845,8 @@ class CNode
845845
// Ping time measurement:
846846
// The pong reply we're expecting, or 0 if no pong expected.
847847
std::atomic<uint64_t> nPingNonceSent{0};
848-
// Time (in usec) the last ping was sent, or 0 if no ping was ever sent.
849-
std::atomic<int64_t> nPingUsecStart{0};
848+
/** When the last ping was sent, or 0 if no ping was ever sent */
849+
std::atomic<std::chrono::microseconds> m_ping_start{std::chrono::microseconds{0}};
850850
// Last measured round-trip time.
851851
std::atomic<int64_t> nPingUsecTime{0};
852852
// Best measured round-trip time.

src/net_processing.cpp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ static constexpr int STALE_RELAY_AGE_LIMIT = 30 * 24 * 60 * 60;
6363
/// Age after which a block is considered historical for purposes of rate
6464
/// limiting block relay. Set to one week, denominated in seconds.
6565
static constexpr int HISTORICAL_BLOCK_AGE = 7 * 24 * 60 * 60;
66-
/** Time between pings automatically sent out for latency probing and keepalive (in seconds). */
67-
static const int PING_INTERVAL = 2 * 60;
66+
/** Time between pings automatically sent out for latency probing and keepalive */
67+
static constexpr std::chrono::minutes PING_INTERVAL{2};
6868
/** The maximum number of entries in a locator */
6969
static const unsigned int MAX_LOCATOR_SZ = 101;
7070
/** The maximum number of entries in an 'inv' protocol message */
@@ -2206,7 +2206,7 @@ void ProcessMessage(
22062206
CNode& pfrom,
22072207
const std::string& msg_type,
22082208
CDataStream& vRecv,
2209-
int64_t nTimeReceived,
2209+
const std::chrono::microseconds time_received,
22102210
const CChainParams& chainparams,
22112211
ChainstateManager& chainman,
22122212
CTxMemPool& mempool,
@@ -3110,7 +3110,7 @@ void ProcessMessage(
31103110
} // cs_main
31113111

31123112
if (fProcessBLOCKTXN)
3113-
return ProcessMessage(pfrom, NetMsgType::BLOCKTXN, blockTxnMsg, nTimeReceived, chainparams, chainman, mempool, connman, banman, interruptMsgProc);
3113+
return ProcessMessage(pfrom, NetMsgType::BLOCKTXN, blockTxnMsg, time_received, chainparams, chainman, mempool, connman, banman, interruptMsgProc);
31143114

31153115
if (fRevertToHeaderProcessing) {
31163116
// Headers received from HB compact block peers are permitted to be
@@ -3385,7 +3385,7 @@ void ProcessMessage(
33853385
}
33863386

33873387
if (msg_type == NetMsgType::PONG) {
3388-
int64_t pingUsecEnd = nTimeReceived;
3388+
const auto ping_end = time_received;
33893389
uint64_t nonce = 0;
33903390
size_t nAvail = vRecv.in_avail();
33913391
bool bPingFinished = false;
@@ -3399,11 +3399,11 @@ void ProcessMessage(
33993399
if (nonce == pfrom.nPingNonceSent) {
34003400
// Matching pong received, this ping is no longer outstanding
34013401
bPingFinished = true;
3402-
int64_t pingUsecTime = pingUsecEnd - pfrom.nPingUsecStart;
3403-
if (pingUsecTime > 0) {
3402+
const auto ping_time = ping_end - pfrom.m_ping_start.load();
3403+
if (ping_time.count() > 0) {
34043404
// Successful ping time measurement, replace previous
3405-
pfrom.nPingUsecTime = pingUsecTime;
3406-
pfrom.nMinPingUsecTime = std::min(pfrom.nMinPingUsecTime.load(), pingUsecTime);
3405+
pfrom.nPingUsecTime = count_microseconds(ping_time);
3406+
pfrom.nMinPingUsecTime = std::min(pfrom.nMinPingUsecTime.load(), count_microseconds(ping_time));
34073407
} else {
34083408
// This should never happen
34093409
sProblem = "Timing mishap";
@@ -3860,7 +3860,7 @@ bool PeerLogicValidation::SendMessages(CNode* pto)
38603860
// RPC ping request by user
38613861
pingSend = true;
38623862
}
3863-
if (pto->nPingNonceSent == 0 && pto->nPingUsecStart + PING_INTERVAL * 1000000 < GetTimeMicros()) {
3863+
if (pto->nPingNonceSent == 0 && pto->m_ping_start.load() + PING_INTERVAL < GetTime<std::chrono::microseconds>()) {
38643864
// Ping automatically sent as a latency probe & keepalive.
38653865
pingSend = true;
38663866
}
@@ -3870,7 +3870,7 @@ bool PeerLogicValidation::SendMessages(CNode* pto)
38703870
GetRandBytes((unsigned char*)&nonce, sizeof(nonce));
38713871
}
38723872
pto->fPingQueued = false;
3873-
pto->nPingUsecStart = GetTimeMicros();
3873+
pto->m_ping_start = GetTime<std::chrono::microseconds>();
38743874
if (pto->nVersion > BIP0031_VERSION) {
38753875
pto->nPingNonceSent = nonce;
38763876
connman->PushMessage(pto, msgMaker.Make(NetMsgType::PING, nonce));

src/test/fuzz/p2p_transport_deserializer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ void test_one_input(const std::vector<uint8_t>& buffer)
3030
pch += handled;
3131
n_bytes -= handled;
3232
if (deserializer.Complete()) {
33-
const int64_t m_time = std::numeric_limits<int64_t>::max();
33+
const std::chrono::microseconds m_time{std::numeric_limits<int64_t>::max()};
3434
const CNetMessage msg = deserializer.GetMessage(Params().MessageStart(), m_time);
3535
assert(msg.m_command.size() <= CMessageHeader::COMMAND_SIZE);
3636
assert(msg.m_raw_message_size <= buffer.size());

src/test/fuzz/process_message.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ void ProcessMessage(
3434
CNode& pfrom,
3535
const std::string& msg_type,
3636
CDataStream& vRecv,
37-
int64_t nTimeReceived,
37+
const std::chrono::microseconds time_received,
3838
const CChainParams& chainparams,
3939
ChainstateManager& chainman,
4040
CTxMemPool& mempool,
@@ -87,7 +87,7 @@ void test_one_input(const std::vector<uint8_t>& buffer)
8787
connman.AddTestNode(p2p_node);
8888
g_setup->m_node.peer_logic->InitializeNode(&p2p_node);
8989
try {
90-
ProcessMessage(p2p_node, random_message_type, random_bytes_data_stream, GetTimeMillis(),
90+
ProcessMessage(p2p_node, random_message_type, random_bytes_data_stream, GetTime<std::chrono::microseconds>(),
9191
Params(), *g_setup->m_node.chainman, *g_setup->m_node.mempool,
9292
g_setup->m_node.connman.get(), g_setup->m_node.banman.get(),
9393
std::atomic<bool>{false});

src/util/time.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ void UninterruptibleSleep(const std::chrono::microseconds& n);
1515
/**
1616
* Helper to count the seconds of a duration.
1717
*
18-
* All durations should be using std::chrono and calling this should generally be avoided in code. Though, it is still
19-
* preferred to an inline t.count() to protect against a reliance on the exact type of t.
18+
* All durations should be using std::chrono and calling this should generally
19+
* be avoided in code. Though, it is still preferred to an inline t.count() to
20+
* protect against a reliance on the exact type of t.
21+
*
22+
* This helper is used to convert durations before passing them over an
23+
* interface that doesn't support std::chrono (e.g. RPC, debug log, or the GUI)
2024
*/
2125
inline int64_t count_seconds(std::chrono::seconds t) { return t.count(); }
26+
inline int64_t count_microseconds(std::chrono::microseconds t) { return t.count(); }
2227

2328
/**
2429
* DEPRECATED

test/functional/feature_bip68_sequence.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ class BIP68Test(BitcoinTestFramework):
3030
def set_test_params(self):
3131
self.num_nodes = 2
3232
self.extra_args = [
33-
["-acceptnonstdtxn=1"],
33+
[
34+
"-acceptnonstdtxn=1",
35+
"-peertimeout=9999", # bump because mocktime might cause a disconnect otherwise
36+
],
3437
["-acceptnonstdtxn=0"],
3538
]
3639

test/functional/feature_maxuploadtarget.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ class MaxUploadTest(BitcoinTestFramework):
3535
def set_test_params(self):
3636
self.setup_clean_chain = True
3737
self.num_nodes = 1
38-
self.extra_args = [["-maxuploadtarget=800", "-acceptnonstdtxn=1"]]
38+
self.extra_args = [[
39+
"-maxuploadtarget=800",
40+
"-acceptnonstdtxn=1",
41+
"-peertimeout=9999", # bump because mocktime might cause a disconnect otherwise
42+
]]
3943
self.supports_cli = False
4044

4145
# Cache for utxos, as the listunspent may take a long time later in the test

test/functional/p2p_ping.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test ping message
6+
"""
7+
8+
import time
9+
10+
from test_framework.messages import (
11+
msg_pong,
12+
)
13+
from test_framework.mininode import (
14+
P2PInterface,
15+
wait_until,
16+
)
17+
from test_framework.test_framework import BitcoinTestFramework
18+
from test_framework.util import assert_equal
19+
20+
PING_INTERVAL = 2 * 60
21+
22+
23+
class msg_pong_corrupt(msg_pong):
24+
def serialize(self):
25+
return b""
26+
27+
28+
class NodePongAdd1(P2PInterface):
29+
def on_ping(self, message):
30+
self.send_message(msg_pong(message.nonce + 1))
31+
32+
33+
class NodeNoPong(P2PInterface):
34+
def on_ping(self, message):
35+
pass
36+
37+
38+
class PingPongTest(BitcoinTestFramework):
39+
def set_test_params(self):
40+
self.setup_clean_chain = True
41+
self.num_nodes = 1
42+
self.extra_args = [['-peertimeout=3']]
43+
44+
def check_peer_info(self, *, pingtime, minping, pingwait):
45+
stats = self.nodes[0].getpeerinfo()[0]
46+
assert_equal(stats.pop('pingtime', None), pingtime)
47+
assert_equal(stats.pop('minping', None), minping)
48+
assert_equal(stats.pop('pingwait', None), pingwait)
49+
50+
def mock_forward(self, delta):
51+
self.mock_time += delta
52+
self.nodes[0].setmocktime(self.mock_time)
53+
54+
def run_test(self):
55+
self.mock_time = int(time.time())
56+
self.mock_forward(0)
57+
58+
self.log.info('Check that ping is sent after connection is established')
59+
no_pong_node = self.nodes[0].add_p2p_connection(NodeNoPong())
60+
self.mock_forward(3)
61+
assert no_pong_node.last_message.pop('ping').nonce != 0
62+
self.check_peer_info(pingtime=None, minping=None, pingwait=3)
63+
64+
self.log.info('Reply without nonce cancels ping')
65+
with self.nodes[0].assert_debug_log(['pong peer=0: Short payload']):
66+
no_pong_node.send_and_ping(msg_pong_corrupt())
67+
self.check_peer_info(pingtime=None, minping=None, pingwait=None)
68+
69+
self.log.info('Reply without ping')
70+
with self.nodes[0].assert_debug_log([
71+
'pong peer=0: Unsolicited pong without ping, 0 expected, 0 received, 8 bytes',
72+
]):
73+
no_pong_node.send_and_ping(msg_pong())
74+
self.check_peer_info(pingtime=None, minping=None, pingwait=None)
75+
76+
self.log.info('Reply with wrong nonce does not cancel ping')
77+
assert 'ping' not in no_pong_node.last_message
78+
with self.nodes[0].assert_debug_log(['pong peer=0: Nonce mismatch']):
79+
# mock time PING_INTERVAL ahead to trigger node into sending a ping
80+
self.mock_forward(PING_INTERVAL + 1)
81+
wait_until(lambda: 'ping' in no_pong_node.last_message)
82+
self.mock_forward(9)
83+
# Send the wrong pong
84+
no_pong_node.send_and_ping(msg_pong(no_pong_node.last_message.pop('ping').nonce - 1))
85+
self.check_peer_info(pingtime=None, minping=None, pingwait=9)
86+
87+
self.log.info('Reply with zero nonce does cancel ping')
88+
with self.nodes[0].assert_debug_log(['pong peer=0: Nonce zero']):
89+
no_pong_node.send_and_ping(msg_pong(0))
90+
self.check_peer_info(pingtime=None, minping=None, pingwait=None)
91+
92+
self.log.info('Check that ping is properly reported on RPC')
93+
assert 'ping' not in no_pong_node.last_message
94+
# mock time PING_INTERVAL ahead to trigger node into sending a ping
95+
self.mock_forward(PING_INTERVAL + 1)
96+
wait_until(lambda: 'ping' in no_pong_node.last_message)
97+
ping_delay = 29
98+
self.mock_forward(ping_delay)
99+
wait_until(lambda: 'ping' in no_pong_node.last_message)
100+
no_pong_node.send_and_ping(msg_pong(no_pong_node.last_message.pop('ping').nonce))
101+
self.check_peer_info(pingtime=ping_delay, minping=ping_delay, pingwait=None)
102+
103+
self.log.info('Check that minping is decreased after a fast roundtrip')
104+
# mock time PING_INTERVAL ahead to trigger node into sending a ping
105+
self.mock_forward(PING_INTERVAL + 1)
106+
wait_until(lambda: 'ping' in no_pong_node.last_message)
107+
ping_delay = 9
108+
self.mock_forward(ping_delay)
109+
wait_until(lambda: 'ping' in no_pong_node.last_message)
110+
no_pong_node.send_and_ping(msg_pong(no_pong_node.last_message.pop('ping').nonce))
111+
self.check_peer_info(pingtime=ping_delay, minping=ping_delay, pingwait=None)
112+
113+
self.log.info('Check that peer is disconnected after ping timeout')
114+
assert 'ping' not in no_pong_node.last_message
115+
self.nodes[0].ping()
116+
wait_until(lambda: 'ping' in no_pong_node.last_message)
117+
with self.nodes[0].assert_debug_log(['ping timeout: 1201.000000s']):
118+
self.mock_forward(20 * 60 + 1)
119+
time.sleep(4) # peertimeout + 1
120+
121+
122+
if __name__ == '__main__':
123+
PingPongTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@
236236
'mempool_compatibility.py',
237237
'rpc_deriveaddresses.py',
238238
'rpc_deriveaddresses.py --usecli',
239+
'p2p_ping.py',
239240
'rpc_scantxoutset.py',
240241
'feature_logging.py',
241242
'p2p_node_network_limited.py',

0 commit comments

Comments
 (0)