Skip to content

Feature Request: Transaction Rate-Limiting #147

@adequatelimited

Description

@adequatelimited

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions