Introduce a new RPC, getnetmsgstats to retrieve network message statistics. This work addresses #26337. More information on the RPC design and implementation can be found in that issue.
Massive thank-you to amitiuttarwar, vasild, and ajtowns for their help on this :pray: Over the course of several months, they have patiently provided a tremendous amount of guidance and assistance in more ways than I can count!
getnetmsgstats RPC
Returns the message count and total number of bytes for sent and received network traffic. Results may optionally be arranged by direction, network, connection type and/or message type.
Arguments
show_only: an optional array of one or more dimensions to show as part of the results. Valid options are: direction, network, message_type, and connection_type. If no dimensions are specified, totals are returned.
Examples
Below are some examples on how the new getnetmsgstats RPC can be used.
<details> <summary>getnetmsgstats</summary>
./src/bitcoin-cli getnetmsgstats
{
"totals": {
"message_count": 1905,
"byte_count": 818941962
}
}
</details>
<details> <summary>getnetmsgstats '["conntype","msgtype"]'</summary>
./src/bitcoin-cli getnetmsgstats '["conntype","msgtype"]'
{
"block-relay-only": {
"addrv2": {
"message_count": 1,
"byte_count": 40
},
"block": {
"message_count": 6,
"byte_count": 9899426
},
"cmpctblock": {
"message_count": 1,
"byte_count": 16615
},
"feefilter": {
"message_count": 1,
"byte_count": 32
},
"getdata": {
"message_count": 1,
"byte_count": 241
},
"getheaders": {
"message_count": 4,
"byte_count": 4212
},
"headers": {
"message_count": 10,
"byte_count": 1303
},
"inv": {
"message_count": 6,
"byte_count": 366
},
"ping": {
"message_count": 4,
"byte_count": 128
},
"pong": {
"message_count": 4,
"byte_count": 128
},
"sendaddrv2": {
"message_count": 4,
"byte_count": 96
},
"sendcmpct": {
"message_count": 6,
"byte_count": 198
},
"sendheaders": {
"message_count": 4,
"byte_count": 96
},
"verack": {
"message_count": 4,
"byte_count": 96
},
"version": {
"message_count": 4,
"byte_count": 507
},
"wtxidrelay": {
"message_count": 4,
"byte_count": 96
}
},
"outbound-full-relay": {
"addr": {
"message_count": 6,
"byte_count": 30302
},
"addrv2": {
"message_count": 10,
"byte_count": 76016
},
"blocktxn": {
"message_count": 1,
"byte_count": 1288086
},
"cmpctblock": {
"message_count": 1,
"byte_count": 16615
},
"feefilter": {
"message_count": 15,
"byte_count": 480
},
"getaddr": {
"message_count": 8,
"byte_count": 192
},
"getblocktxn": {
"message_count": 1,
"byte_count": 2515
},
"getdata": {
"message_count": 79,
"byte_count": 16951
},
"getheaders": {
"message_count": 15,
"byte_count": 15795
},
"headers": {
"message_count": 20,
"byte_count": 2039
},
"inv": {
"message_count": 134,
"byte_count": 58826
},
"notfound": {
"message_count": 7,
"byte_count": 787
},
"other": {
"message_count": 6,
"byte_count": 438
},
"ping": {
"message_count": 15,
"byte_count": 480
},
"pong": {
"message_count": 14,
"byte_count": 448
},
"sendaddrv2": {
"message_count": 10,
"byte_count": 240
},
"sendcmpct": {
"message_count": 19,
"byte_count": 627
},
"sendheaders": {
"message_count": 14,
"byte_count": 336
},
"tx": {
"message_count": 398,
"byte_count": 211333
},
"verack": {
"message_count": 16,
"byte_count": 384
},
"version": {
"message_count": 17,
"byte_count": 2151
},
"wtxidrelay": {
"message_count": 10,
"byte_count": 240
}
}
}
</details>
<details> <summary>getnetmsgstats '["network", "direction", "connection_type", "message_type"]'</summary>
./src/bitcoin-cli getnetmsgstats '["network", "direction", "connection_type", "message_type"]'
{
"ipv4": {
"received": {
"block-relay-only": {
"addrv2": {
"message_count": 5,
"byte_count": 227
},
"block": {
"message_count": 6,
"byte_count": 9899426
},
"cmpctblock": {
"message_count": 2,
"byte_count": 25184
},
"feefilter": {
"message_count": 1,
"byte_count": 32
},
"getheaders": {
"message_count": 2,
"byte_count": 2106
},
"headers": {
"message_count": 6,
"byte_count": 1041
},
"inv": {
"message_count": 3,
"byte_count": 183
},
"ping": {
"message_count": 6,
"byte_count": 192
},
"pong": {
"message_count": 6,
"byte_count": 192
},
"sendaddrv2": {
"message_count": 2,
"byte_count": 48
},
"sendcmpct": {
"message_count": 3,
"byte_count": 99
},
"sendheaders": {
"message_count": 2,
"byte_count": 48
},
"verack": {
"message_count": 2,
"byte_count": 48
},
"version": {
"message_count": 2,
"byte_count": 253
},
"wtxidrelay": {
"message_count": 2,
"byte_count": 48
}
},
"outbound-full-relay": {
"addr": {
"message_count": 4,
"byte_count": 30222
},
"addrv2": {
"message_count": 26,
"byte_count": 148422
},
"blocktxn": {
"message_count": 2,
"byte_count": 3752987
},
"cmpctblock": {
"message_count": 2,
"byte_count": 25184
},
"feefilter": {
"message_count": 11,
"byte_count": 352
},
"getdata": {
"message_count": 24,
"byte_count": 2184
},
"getheaders": {
"message_count": 11,
"byte_count": 11583
},
"headers": {
"message_count": 20,
"byte_count": 2120
},
"inv": {
"message_count": 275,
"byte_count": 116207
},
"notfound": {
"message_count": 9,
"byte_count": 981
},
"other": {
"message_count": 44,
"byte_count": 3430
},
"ping": {
"message_count": 20,
"byte_count": 640
},
"pong": {
"message_count": 20,
"byte_count": 640
},
"sendaddrv2": {
"message_count": 9,
"byte_count": 216
},
"sendcmpct": {
"message_count": 18,
"byte_count": 594
},
"sendheaders": {
"message_count": 11,
"byte_count": 264
},
"tx": {
"message_count": 1161,
"byte_count": 596142
},
"verack": {
"message_count": 12,
"byte_count": 288
},
"version": {
"message_count": 12,
"byte_count": 1536
},
"wtxidrelay": {
"message_count": 9,
"byte_count": 216
}
}
},
"sent": {
"block-relay-only": {
"getdata": {
"message_count": 1,
"byte_count": 241
},
"getheaders": {
"message_count": 2,
"byte_count": 2106
},
"headers": {
"message_count": 6,
"byte_count": 474
},
"inv": {
"message_count": 3,
"byte_count": 183
},
"ping": {
"message_count": 6,
"byte_count": 192
},
"pong": {
"message_count": 6,
"byte_count": 192
},
"sendaddrv2": {
"message_count": 2,
"byte_count": 48
},
"sendcmpct": {
"message_count": 3,
"byte_count": 99
},
"sendheaders": {
"message_count": 2,
"byte_count": 48
},
"verack": {
"message_count": 2,
"byte_count": 48
},
"version": {
"message_count": 2,
"byte_count": 254
},
"wtxidrelay": {
"message_count": 2,
"byte_count": 48
}
},
"outbound-full-relay": {
"addr": {
"message_count": 4,
"byte_count": 250
},
"addrv2": {
"message_count": 19,
"byte_count": 938
},
"feefilter": {
"message_count": 12,
"byte_count": 384
},
"getaddr": {
"message_count": 12,
"byte_count": 288
},
"getblocktxn": {
"message_count": 2,
"byte_count": 3883
},
"getdata": {
"message_count": 249,
"byte_count": 48813
},
"getheaders": {
"message_count": 12,
"byte_count": 12636
},
"headers": {
"message_count": 13,
"byte_count": 1297
},
"inv": {
"message_count": 464,
"byte_count": 166868
},
"ping": {
"message_count": 21,
"byte_count": 672
},
"pong": {
"message_count": 20,
"byte_count": 640
},
"sendaddrv2": {
"message_count": 9,
"byte_count": 216
},
"sendcmpct": {
"message_count": 13,
"byte_count": 429
},
"sendheaders": {
"message_count": 11,
"byte_count": 264
},
"tx": {
"message_count": 44,
"byte_count": 18966
},
"verack": {
"message_count": 12,
"byte_count": 288
},
"version": {
"message_count": 13,
"byte_count": 1651
},
"wtxidrelay": {
"message_count": 9,
"byte_count": 216
}
}
}
}
}
</details>
Things to consider
RPC behavior: Should we allow dimensions to be rearraged?
When it comes time to gather up the RPC results, @vasild has provided an alternate implementation that uses an array instead of the MultiMap structure. This results in two changes:
- using the stack over the heap (yay!)
- enforcing a strict ordering of dimensions (direction, network, connection type, message type)
Aside from being good for memory, the reasoning here is allowing users to rearrange dimensions may be too confusing. I personally really like the ability to rearrange dimensions, which is why I have not integrated that solution into this PR, but am open to whatever the larger community prefers :)
Locking & Consistency Beyond Individual Values
With atomics, we know we can rely on the values for individual stats like bye count and message count. However, the byte count and message count may not always match. For example, let’s say we currently have 5 messages, and 100 bytes:
1. A new 20 byte message comes in. First the byte count is incremented to 120.
2. A request to read the stats comes in. The RPC returns message count 5 and byte count 120.
3. The message count is incremented to 6 in response to the new message that came in at step 1.
The read operation did not return accurate data! Unlike the torn write example for a single value, It returned data that was true for some point in time, it’s just that the values for message count and byte count were inconsistent.
To solve this, it’s actually not enough to lock the MsgStat struct. It's my understanding that you need a mutex to protect the entire NetStats data structure.
The trade off here isn’t attractive. A lock would halt network threads that are doing important stuff, all for the sake of consistent stats. Even for reads, we would have to stop writes. We’d end up stopping threads that are doing important things for something that is not that important.
Another thing to consider is how often will this RPC be called? If it’s once in a blue moon, then such a mutex is probably fine. But for a live dashboard that is making a call every second, this is not acceptable.
Making Enum Indices Safe and Reliable
src/net.cpp contains a bunch of *To/FromIndex() methods that convert an enum to an index number. I’ve decided to be explicit about these conversions because enums are not guaranteed to start at zero and it’s not enough to simply associate the first value in an enum with a number.
To protect against potential gaps or overlaps in numbering, every single enum value must be assigned an index. This is the only way to guarantee that the numbering is safe and reliable.
Instead of updating the existing Network and ConnectionType enums to assign indices to each enum value, and risk unintentionally modifying the behavior of code that uses these enums, I’ve opted for the conversion methods. This also narrows the scope of the conversion methods, making changed behavior easier to spot if the indices are modified.
I’m interested in feedback on this. The *To/FromIndex() methods have low review and maintenance cost, but I’m unsure if I’ve correctly evaluated the risks associated with updating the Network and ConnectionType enums <can insert an example of assigning specific indices to each enum value>. Also open to discussion on if there is a better place for these conversion methods to live.