Changing nScore and nBestScore to a fixed-width integer guarantees to be the same size on any architecture and avoids UB.
Addresses issue: #24049
Changing nScore and nBestScore to a fixed-width integer guarantees to be the same size on any architecture and avoids UB.
Addresses issue: #24049
Concept ACK
It makes sense to convert nScore
to int64_t from int to avoid UB. However, I shall take a further look in the codebase to check if doing so might not be causing a yet unforeseen issue.
139@@ -140,7 +140,7 @@ bool GetLocal(CService& addr, const CNetAddr *paddrPeer)
140 LOCK(cs_mapLocalHost);
141 for (const auto& entry : mapLocalHost)
142 {
143- int nScore = entry.second.nScore;
144+ int64_t nScore = entry.second.nScore;
nBestScore = nScore;
below, I think that nBestScore
should be changed to int64_t
as well.
81d20b8e2f0097867b234bdc02cb2d7aa95b4a51
Changed nBestScore type to int64_t per @mzumsande comment.
Concept ACK
It makes sense to convert
nScore
to int64_t from int to avoid UB. However, I shall take a further look in the codebase to check if doing so might not be causing a yet unforeseen issue.
The scope of nBestScore is limited to the function:
0bool GetLocal(CService& addr, const CNetAddr *paddrPeer)
https://github.com/bitcoin/bitcoin/blob/81d20b8e2f0097867b234bdc02cb2d7aa95b4a51/src/net.cpp#L137
which returns a boolean based on the evaluation:
return nBestScore >= 0;
crACK 81d20b8
Changing nScore
and nBestScore
to a fixed-width integer guarantees to be the same size on any architecture and avoids UB.
Since the type of nScore
is changed to int64_t, I think it’s logical to change the return type of GetnScore from int to int64_t.
Line 194 in src/net.cpp
:
0static int GetnScore(const CService& addr)
1{
2 LOCK(cs_mapLocalHost);
3 const auto it = mapLocalHost.find(addr);
4 return (it != mapLocalHost.end()) ? it->second.nScore : 0;
5}
1e0703baaab58915f43831c32bfd789015a8d483
updated function:
0static int GetnScore(const CService& addr)
return type to int64_t per @shaavan comment.
ACK 1e0703baaab58915f43831c32bfd789015a8d483
Changes since my last review:
GetnScore
from int to int64_t to match the nScore
’s type.SaturatingAdd
?
Saturating means once an IP reaches max, it won’t ever get swapped out for a newer IP, right?
Maybe instead, when we would increment beyond max, we should subtract the lowest (>LOCAL_MAX
) current score from all mapLocalHost
entries? I think that would preserve the current behaviour.
guarantees to be the same size
Does this matter?
ACK 1e0703baaab58915f43831c32bfd789015a8d483 code review, rebased, debug build, ran unit tests.
That said, changing the one line that increments nScore
in SeenLocal()
to use SaturatingAdd
(recently added in #24224) looks to me like a preferable and more minimal change.
An additional reason might be that if any math operations to nScore
are added in the future, the C++ tendency to auto-convert to int might be a good reason to leave nScore
as int here, if I’m not confused and lacking caffeine.
My thought is something like this:
0--- a/src/net.cpp
1+++ b/src/net.cpp
2@@ -349,6 +349,20 @@ bool SeenLocal(const CService& addr)
3 LOCK(g_maplocalhost_mutex);
4 const auto it = mapLocalHost.find(addr);
5 if (it == mapLocalHost.end()) return false;
6+ if (it->second.nScore == std::numeric_limits<int>::max()) {
7+ int lowest_inc_score = std::numeric_limits<int>::max();
8+ for (auto& localinfo : mapLocalHost) {
9+ if (localinfo.second.nScore > LOCAL_MAX && localinfo.second.nScore < lowest_inc_score) {
10+ lowest_inc_score = localinfo.second.nScore;
11+ }
12+ }
13+ lowest_inc_score -= LOCAL_MAX;
14+ for (auto& localinfo : mapLocalHost) {
15+ if (localinfo.second.nScore > LOCAL_MAX) {
16+ localinfo.second.nScore -= lowest_inc_score;
17+ }
18+ }
19+ }
20 ++it->second.nScore;
21 return true;
22 }
But I’m not 100% sure of it yet.
My thought is something like this:
0--- a/src/net.cpp 1+++ b/src/net.cpp 2@@ -349,6 +349,20 @@ bool SeenLocal(const CService& addr) 3 LOCK(g_maplocalhost_mutex); 4 const auto it = mapLocalHost.find(addr); 5 if (it == mapLocalHost.end()) return false; 6+ if (it->second.nScore == std::numeric_limits<int>::max()) { 7+ int lowest_inc_score = std::numeric_limits<int>::max(); 8+ for (auto& localinfo : mapLocalHost) { 9+ if (localinfo.second.nScore > LOCAL_MAX && localinfo.second.nScore < lowest_inc_score) { 10+ lowest_inc_score = localinfo.second.nScore; 11+ } 12+ } 13+ lowest_inc_score -= LOCAL_MAX; 14+ for (auto& localinfo : mapLocalHost) { 15+ if (localinfo.second.nScore > LOCAL_MAX) { 16+ localinfo.second.nScore -= lowest_inc_score; 17+ } 18+ } 19+ } 20 ++it->second.nScore; 21 return true; 22 }
But I’m not 100% sure of it yet.
Does the code inside the if (INT_MAX == it->second.nscore)
need to have linear ([or] is it binomial?) time complexity? You’re updating EVERY node in a map because the nScore
in ONE of the is equal to INT_MAX
? If you only intend to keep the ordering: there are many ways to do it in constant, time.
All of that, and it happens to be a breaking change.
0 ++reinterpret_cast<unsigned&>(it->second.nScore);
Keeps everything unchanged, gets rid of the undefined behavior making it (until c++20: implementation specified; since c++20: well-defined) behavior.
What do you think?
EDIT: Removed a paragraph since the problem I described in it couldn’t happen without a data race, anyway.