Skip to content

ltc: Send() socket deadline + desync-safe teardown (parity with dash #781)#784

Merged
frstrtr merged 1 commit into
masterfrom
ltc/send-socket-deadline
Jul 21, 2026
Merged

ltc: Send() socket deadline + desync-safe teardown (parity with dash #781)#784
frstrtr merged 1 commit into
masterfrom
ltc/send-socket-deadline

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 21, 2026

Copy link
Copy Markdown
Owner

P1 — live LTC-DOGE prod reference: RPC socket deadline

Propagates the DASH #781 (9ac8b3dd) socket-deadline class to LTC, the P1 lane
in the cross-coin io/RPC reconciliation audit. LTC already has the class-A
thread_pool decouple and the class-B m_rpc_mutex; the remaining gap is the
class-C deadline.

Problem

ltc::coin::NodeRPC::Send() has no deadline — a wedged-but-connected
litecoind/dogecoind (socket open, no bytes) hangs the blocking http::read
forever → permanent refresh-wedge + SIGINT-hang on join, on the live merged-
mining node.

Change — port DASH's mechanism verbatim

  • apply_socket_timeouts() — forces the socket back to BLOCKING mode
    (async_connect leaves asio's non_blocking flag set, under which SO_*TIMEO
    is a no-op) then sets kernel SO_RCVTIMEO/SO_SNDTIMEO =
    RPC_IO_TIMEOUT_SECONDS (12). Called after every successful (re)connect (the
    async_connect handler before check(), and sync_reconnect()). beast's
    synchronous read_some/write_some do not honour
    tcp_stream::expires_after (async-only), so the kernel timeout is the robust
    bound. #ifndef _WIN32 guarded (Linux release target; Windows keeps the
    pre-parity no-deadline behaviour).
  • close_stream() — desync-safe teardown on the final-attempt Send()
    exits (both write-fail and read-fail). This is the critical half — the
    deadline alone reintroduces dash: decouple blocking coin-RPC + heavy work off the stratum io_context (io-thread-decouple) #781's item-2b off-by-one desync.

Why the desync applies to LTC (verified, not assumed)

LTC's NodeRPC shares DASH's shape: const std::string ID = "curltest" (a
constant request id) driving jsonrpccxx::JsonRpcClient, which never validates
response ids. A timed-out-but-still-open stream (request written, response
unread) reused by the next Send() would read this request's late response
→ permanent off-by-one: a GBT parses a getbestblockhash hash-string → zeroed
work → a stuck "honest set-gap" outage until the connection churns.
close_stream() tears the socket down so the next Send() write-fails into a
clean sync_reconnect(). LTC already had m_rpc_mutex (class B) — no mutex
work needed.

Consensus-neutral

Transport/liveness only — zero wire-byte change; strict added-code diff-grep
for share/target/payout/vardiff/reward/coinbase/merkle/nbits/difficulty is
empty. Satisfies the pre-v36 transition rule.

Scope note — LTC class-D (WebServer) is a separate follow-on

LTC's miner-facing server is core::WebServer, not core::StratumServer,
so the #781 core reap knobs do NOT reach LTC. Its zombie-session class needs a
WebServer-level keepalive + idle-reap fix — queued, out of scope for this PR
(class-C deadline only).

Build/test

c2pool-ltc builds clean with the change compiled in; LTC-lane regression
(test_template_builder 35/35, test_doge_chain 37/37) green. NodeRPC is not
unit-testable without a live daemon socket.

Draft — not for merge/deploy; LTC-DOGE prod is an operator tap. Gets an
LTC-parity review first.

…781)

LTC (the live LTC-DOGE prod reference) already has the thread_pool decouple
and m_rpc_mutex, but NodeRPC::Send() had no deadline: a wedged-but-connected
litecoind/dogecoind (socket open, no bytes) hangs the blocking http::read
forever -> permanent refresh-wedge + SIGINT-hang on join.

Port DASH #781's mechanism verbatim into impl/ltc/coin/rpc.cpp:

- apply_socket_timeouts(): force the socket back to BLOCKING mode
  (async_connect leaves asio's non_blocking flag set, under which SO_*TIMEO is
  a no-op) then set kernel SO_RCVTIMEO/SO_SNDTIMEO = RPC_IO_TIMEOUT_SECONDS
  (12). Called after every successful (re)connect. beast's synchronous
  read_some/write_some do not honour tcp_stream::expires_after (async-only),
  so the kernel timeout is the robust bound. #ifndef _WIN32 guarded (Linux
  release target; Windows keeps pre-parity no-deadline behaviour).

- close_stream(): the desync-safe teardown on the FINAL-attempt Send() exits
  (both write-fail and read-fail). This is the critical half: the deadline
  alone reintroduces the off-by-one response desync. LTC's NodeRPC shares
  DASH's shape -- the constant id "curltest" driving jsonrpccxx, which never
  validates response ids -- so a timed-out-but-open stream reused by the next
  Send() would read this request's late response (a GBT parsing a
  getbestblockhash string -> zeroed work -> a stuck honest set-gap outage).
  Tearing the socket down makes the next Send() write-fail into a clean
  sync_reconnect().

LTC already had m_rpc_mutex (class B), so no mutex work was needed.
Transport/liveness only -- zero wire-byte change, consensus-neutral (no
share/target/payout/vardiff logic touched).
@frstrtr
frstrtr marked this pull request as ready for review July 21, 2026 07:50
@frstrtr
frstrtr merged commit 4cda475 into master Jul 21, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant