66
77#include < core/log.hpp>
88#include < core/hash.hpp>
9+
10+ #ifndef _WIN32
11+ #include < sys/socket.h> // setsockopt SO_SND/RCVTIMEO (Send() deadline)
12+ #include < sys/time.h> // struct timeval
13+ #endif
14+
915namespace ltc
1016{
1117
@@ -67,6 +73,10 @@ void NodeRPC::connect(NetService address, std::string userpass)
6773 {
6874 try
6975 {
76+ // Bound the synchronous Send() I/O BEFORE check()
77+ // runs its first RPC, so a wedged litecoind cannot
78+ // hang the caller (io thread or thread_pool worker).
79+ apply_socket_timeouts ();
7080 if (check ())
7181 {
7282 m_connected = true ;
@@ -128,9 +138,51 @@ void NodeRPC::sync_reconnect()
128138 LOG_WARNING << " CoindRPC sync_reconnect connect failed: " << ec.message ();
129139 return ;
130140 }
141+ apply_socket_timeouts (); // re-arm the Send() deadline on the fresh socket
131142 LOG_INFO << " CoindRPC reconnected (sync)" ;
132143}
133144
145+ void NodeRPC::close_stream ()
146+ {
147+ // Same teardown idiom as sync_reconnect()/the destructor. m_connected is
148+ // cleared so the next Send() write-fails into a clean sync_reconnect().
149+ beast::error_code ec;
150+ m_stream.socket ().shutdown (io::ip::tcp::socket::shutdown_both, ec);
151+ m_stream.close ();
152+ m_connected = false ;
153+ }
154+
155+ void NodeRPC::apply_socket_timeouts ()
156+ {
157+ // Force the socket back to BLOCKING mode, then set kernel send/receive
158+ // timeouts. async_connect (connect()) leaves asio's non_blocking flag set;
159+ // under it a synchronous recv returns EAGAIN immediately and SO_RCVTIMEO is
160+ // a no-op. In blocking mode the sync http::write/http::read inside Send() do
161+ // true blocking send()/recv() which the kernel bounds by SO_SND/RCVTIMEO,
162+ // returning an error after RPC_IO_TIMEOUT_SECONDS instead of hanging on a
163+ // wedged litecoind/dogecoind. (beast tcp_stream::expires_after cannot be
164+ // used: it only governs ASYNC ops -- the sync path calls socket.read_some()
165+ // directly.) Per-operation inactivity timeout: a large-but-steadily-arriving
166+ // GBT does NOT trip it; only a genuine stall does. POSIX (SO_*TIMEO takes a
167+ // timeval); guarded off Windows (no <sys/time.h>/timeval, and Winsock
168+ // SO_RCVTIMEO takes a DWORD) -- the deploy target is Linux, and macOS keeps
169+ // the real deadline. On Windows this is a no-op (pre-parity behaviour).
170+ #ifndef _WIN32
171+ if (!m_stream.socket ().is_open ())
172+ return ;
173+ boost::system::error_code ec;
174+ m_stream.socket ().non_blocking (false , ec); // guarantee SO_*TIMEO applies
175+ if (ec)
176+ LOG_WARNING << " CoindRPC: could not set blocking mode: " << ec.message ();
177+ struct timeval tv;
178+ tv.tv_sec = RPC_IO_TIMEOUT_SECONDS ;
179+ tv.tv_usec = 0 ;
180+ const int fd = m_stream.socket ().native_handle ();
181+ ::setsockopt (fd, SOL_SOCKET , SO_RCVTIMEO , &tv, sizeof (tv));
182+ ::setsockopt (fd, SOL_SOCKET , SO_SNDTIMEO , &tv, sizeof (tv));
183+ #endif
184+ }
185+
134186std::string NodeRPC::Send (const std::string &request)
135187{
136188 std::lock_guard<std::mutex> _rpc_lock (m_rpc_mutex);
@@ -151,6 +203,10 @@ std::string NodeRPC::Send(const std::string &request)
151203 sync_reconnect ();
152204 continue ;
153205 }
206+ // Deadline-desync guard (see the read-fail path below): tear the
207+ // socket down before giving up so a partially-written request can
208+ // never be answered into a LATER Send() on a reused connection.
209+ close_stream ();
154210 return {};
155211 }
156212
@@ -169,6 +225,17 @@ std::string NodeRPC::Send(const std::string &request)
169225 sync_reconnect ();
170226 continue ;
171227 }
228+ // Deadline-desync guard (SO_RCVTIMEO deadline hazard): this final
229+ // attempt has already WRITTEN the request into litecoind but its
230+ // response is unread (read timed out). jsonrpccxx reuses the constant
231+ // id "curltest" and never validates response ids, so reusing this
232+ // open connection would make the NEXT Send() read THIS request's late
233+ // response -> permanent off-by-one desync (a GBT would parse a
234+ // getbestblockhash string -> zeroed work -> a stuck "honest set-gap"
235+ // outage until the connection churns). Tear the socket down so the
236+ // next Send() write-fails into a clean sync_reconnect(). Already
237+ // under m_rpc_mutex.
238+ close_stream ();
172239 return {};
173240 }
174241
0 commit comments