Background
Referenced from Audit item F-08 from the March 2026 audit, this turned out to be a feature request rather than a bug.
Overview
Each OP_TX packet received by the node triggers a full WOTS+ signature verification — 67 SHA-256 chain steps over 2144 bytes each (~6,700-17,000 SHA-256 operations). This verification occurs inline in the main server loop via gettx() → process_tx() → tx_val() → tx_val__wots() → wots_pk_from_sig().
Currently, the node relies on pinklisting to handle abusive peers: an IP that sends an invalid transaction is pinklisted via goto bad2 in gettx(), and subsequent connections from that IP are rejected at the pinklisted() check before the handshake completes. The pink lists are fixed-size circular buffers (CPINKLEN = LPINKLEN = EPINKLEN = 100 entries each, 300 total across src/peer.c).
A per-IP transaction rate limiter would add defense-in-depth by bounding CPU consumption from any single peer before pinklisting is triggered, and would remain effective even if the pink list capacity is exhausted by a large number of distinct source IPs.
Proposed implementation
Per-IP rate limiter
A lightweight hash table tracking recent TX submissions per IP address:
#define TX_RATE_WINDOW 60 /* seconds */
#define TX_RATE_LIMIT 100 /* max TX per IP per window */
#define TX_RATE_SLOTS 256 /* hash table slots */
static struct {
word32 ip;
word32 count;
time_t window_start;
} tx_rate_table[TX_RATE_SLOTS];
static int tx_rate_check(word32 ip) {
word32 slot = ip % TX_RATE_SLOTS;
time_t now = time(NULL);
if (tx_rate_table[slot].ip != ip ||
difftime(now, tx_rate_table[slot].window_start) >= TX_RATE_WINDOW) {
tx_rate_table[slot].ip = ip;
tx_rate_table[slot].count = 1;
tx_rate_table[slot].window_start = now;
return VEOK;
}
if (++tx_rate_table[slot].count > TX_RATE_LIMIT) {
return VERROR; /* rate limited */
}
return VEOK;
}
Integration point
In gettx(), before process_tx() is called in the OP_TX handler:
case OP_TX: {
Nlogins++;
if (tx_rate_check(np->ip) != VEOK) {
pdebug("%s rate limited (OP_TX)", np->id);
goto bad2;
}
status = process_tx(np);
...
}
Design considerations
- Threshold tuning: 100 TX/minute/IP is a starting point. Should this be configurable at runtime?
- Rate-limited disposition: Should rate-limited IPs be pinklisted (
goto bad2) or silently dropped (return 1)?
- Hash table sizing: 256 slots may collide under heavy peer load. Consider whether a larger table or a different collision strategy is warranted.
- Hash table collisions: The current design uses direct-mapped slots (
ip % TX_RATE_SLOTS). Two different IPs mapping to the same slot will reset each other's counters, weakening the rate limit for both. An alternative is a small associative set per slot.
Background
Referenced from Audit item F-08 from the March 2026 audit, this turned out to be a feature request rather than a bug.
Overview
Each
OP_TXpacket received by the node triggers a full WOTS+ signature verification — 67 SHA-256 chain steps over 2144 bytes each (~6,700-17,000 SHA-256 operations). This verification occurs inline in the main server loop viagettx()→process_tx()→tx_val()→tx_val__wots()→wots_pk_from_sig().Currently, the node relies on pinklisting to handle abusive peers: an IP that sends an invalid transaction is pinklisted via
goto bad2ingettx(), and subsequent connections from that IP are rejected at thepinklisted()check before the handshake completes. The pink lists are fixed-size circular buffers (CPINKLEN = LPINKLEN = EPINKLEN = 100entries each, 300 total acrosssrc/peer.c).A per-IP transaction rate limiter would add defense-in-depth by bounding CPU consumption from any single peer before pinklisting is triggered, and would remain effective even if the pink list capacity is exhausted by a large number of distinct source IPs.
Proposed implementation
Per-IP rate limiter
A lightweight hash table tracking recent TX submissions per IP address:
Integration point
In
gettx(), beforeprocess_tx()is called in theOP_TXhandler:Design considerations
goto bad2) or silently dropped (return 1)?ip % TX_RATE_SLOTS). Two different IPs mapping to the same slot will reset each other's counters, weakening the rate limit for both. An alternative is a small associative set per slot.