Skip to content

Commit aa24313

Browse files
test: startup order and critical network hold patterns (12 tests)
Tests the setMode-before-setRpc ordering that caused the 15-min burst sync delay bug. Also tests network hold stacking, HTLC safety margins vs burst intervals, and hold patterns for send/receive/channel operations.
1 parent a815aa7 commit aa24313

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package com.pocketnode.power
2+
3+
import org.junit.Assert.*
4+
import org.junit.Test
5+
6+
/**
7+
* Tests for startup order and critical hold patterns.
8+
*
9+
* Bug context: setRpc() was called before setMode() in BitcoindService startup.
10+
* setRpc() checks activeScope (set by setMode()) and returns early if null.
11+
* This meant burst cycling never started on cold boot — network stayed fully
12+
* open for ~15 min until a UI screen called setRpc() again.
13+
*
14+
* Fixed by swapping the order: setMode() first (sets scope), then setRpc().
15+
*/
16+
class StartupOrderTest {
17+
18+
// ── Startup Sequence ─────────────────────────────────────────────
19+
20+
@Test
21+
fun `setRpc requires activeScope - returns early if null`() {
22+
// Simulates the bug: setRpc() called before setMode()
23+
var activeScope: Any? = null
24+
var burstStarted = false
25+
26+
// setRpc logic: checks activeScope
27+
fun setRpc() {
28+
val mode = PowerModeManager.Mode.LOW
29+
val burstActive = false
30+
val initialSyncHold = false
31+
if (mode != PowerModeManager.Mode.MAX && !burstActive && !initialSyncHold) {
32+
val scope = activeScope ?: return // <-- returns early if null
33+
burstStarted = true
34+
}
35+
}
36+
37+
setRpc()
38+
assertFalse("Burst should NOT start when activeScope is null", burstStarted)
39+
}
40+
41+
@Test
42+
fun `setMode then setRpc - correct order starts burst`() {
43+
var activeScope: Any? = null
44+
var burstStarted = false
45+
46+
// setMode sets the scope
47+
fun setMode() {
48+
activeScope = Object() // non-null scope
49+
}
50+
51+
// setRpc checks the scope
52+
fun setRpc() {
53+
val mode = PowerModeManager.Mode.LOW
54+
val burstActive = false
55+
val initialSyncHold = false
56+
if (mode != PowerModeManager.Mode.MAX && !burstActive && !initialSyncHold) {
57+
val scope = activeScope ?: return
58+
burstStarted = true
59+
}
60+
}
61+
62+
setMode() // FIRST: sets activeScope
63+
setRpc() // SECOND: checks activeScope, starts burst
64+
assertTrue("Burst SHOULD start when setMode() called first", burstStarted)
65+
}
66+
67+
@Test
68+
fun `wrong order - setRpc then setMode - burst never starts from setRpc`() {
69+
var activeScope: Any? = null
70+
var burstStartedFromSetRpc = false
71+
var burstStartedFromSetMode = false
72+
73+
fun setRpc() {
74+
val mode = PowerModeManager.Mode.LOW
75+
val burstActive = false
76+
val initialSyncHold = false
77+
if (mode != PowerModeManager.Mode.MAX && !burstActive && !initialSyncHold) {
78+
val scope = activeScope ?: return
79+
burstStartedFromSetRpc = true
80+
}
81+
}
82+
83+
fun setMode() {
84+
activeScope = Object()
85+
// setMode calls applyMode which also starts burst,
86+
// but the RPC client might not be ready yet
87+
burstStartedFromSetMode = true
88+
}
89+
90+
setRpc() // WRONG ORDER: activeScope still null
91+
setMode() // Sets scope but burst needs RPC
92+
93+
assertFalse("setRpc should have returned early", burstStartedFromSetRpc)
94+
assertTrue("setMode starts its own burst path", burstStartedFromSetMode)
95+
}
96+
97+
// ── Both Startup Paths ───────────────────────────────────────────
98+
99+
@Test
100+
fun `cold start path follows correct order`() {
101+
// Verify the pattern from BitcoindService.startBitcoind()
102+
// Line 251-252 (after fix):
103+
// pmm.setMode(modeFlow.value, serviceScope) // FIRST
104+
// pmm.setRpc(rpc) // SECOND
105+
val steps = mutableListOf<String>()
106+
107+
steps.add("start_bitcoind")
108+
steps.add("create_rpc_client")
109+
steps.add("start_monitors")
110+
steps.add("setMode") // Sets activeScope
111+
steps.add("setRpc") // Checks activeScope, starts burst
112+
113+
val setModeIdx = steps.indexOf("setMode")
114+
val setRpcIdx = steps.indexOf("setRpc")
115+
assertTrue("setMode must come before setRpc", setModeIdx < setRpcIdx)
116+
}
117+
118+
@Test
119+
fun `attach path follows correct order`() {
120+
// Verify the pattern from BitcoindService attach-to-running path
121+
val steps = mutableListOf<String>()
122+
123+
steps.add("detect_running_bitcoind")
124+
steps.add("rpc_health_check")
125+
steps.add("start_monitors")
126+
steps.add("setMode")
127+
steps.add("setRpc")
128+
129+
val setModeIdx = steps.indexOf("setMode")
130+
val setRpcIdx = steps.indexOf("setRpc")
131+
assertTrue("setMode must come before setRpc (attach path)", setModeIdx < setRpcIdx)
132+
}
133+
134+
// ── Network Hold for Lightning Operations ────────────────────────
135+
136+
@Test
137+
fun `receive hold pattern - acquire before generate, release after timeout or receive`() {
138+
var holdCount = 0
139+
var invoiceGenerated = false
140+
var paymentReceived = false
141+
142+
// Generate invoice on non-MAX mode
143+
val mode = PowerModeManager.Mode.LOW
144+
if (mode != PowerModeManager.Mode.MAX) {
145+
holdCount++
146+
}
147+
assertEquals("Hold acquired for receive", 1, holdCount)
148+
149+
invoiceGenerated = true
150+
assertTrue("Invoice generated while hold active", invoiceGenerated)
151+
152+
// Simulate payment received
153+
paymentReceived = true
154+
if (paymentReceived) {
155+
holdCount--
156+
}
157+
assertEquals("Hold released after receive", 0, holdCount)
158+
}
159+
160+
@Test
161+
fun `send hold pattern - acquire on screen entry, release on exit`() {
162+
var holdCount = 0
163+
164+
// Enter send screen
165+
val mode = PowerModeManager.Mode.LOW
166+
if (mode != PowerModeManager.Mode.MAX) {
167+
holdCount++
168+
}
169+
assertEquals(1, holdCount)
170+
171+
// Payment completes, screen exits
172+
holdCount--
173+
assertEquals("Hold released on exit", 0, holdCount)
174+
}
175+
176+
@Test
177+
fun `channel open hold pattern - acquire, wait for confirmed, release after buffer`() {
178+
var holdCount = 0
179+
var channelReady = false
180+
181+
// Start channel open
182+
holdCount++
183+
assertEquals(1, holdCount)
184+
185+
// Channel confirmed
186+
channelReady = true
187+
188+
// 10s buffer then release
189+
if (channelReady) {
190+
holdCount--
191+
}
192+
assertEquals("Hold released after channel ready + buffer", 0, holdCount)
193+
}
194+
195+
@Test
196+
fun `MAX mode skips all holds`() {
197+
var holdCount = 0
198+
val mode = PowerModeManager.Mode.MAX
199+
200+
// Receive flow
201+
if (mode != PowerModeManager.Mode.MAX) {
202+
holdCount++
203+
}
204+
assertEquals("MAX mode should not acquire hold", 0, holdCount)
205+
206+
// Send flow
207+
if (mode != PowerModeManager.Mode.MAX) {
208+
holdCount++
209+
}
210+
assertEquals("MAX mode should not acquire hold", 0, holdCount)
211+
}
212+
213+
@Test
214+
fun `overlapping holds stack correctly`() {
215+
var holdCount = 0
216+
217+
// User opens receive screen (hold 1)
218+
holdCount++
219+
assertEquals(1, holdCount)
220+
221+
// While waiting, user also triggers channel operation (hold 2)
222+
holdCount++
223+
assertEquals(2, holdCount)
224+
225+
// Receive completes (release 1)
226+
holdCount--
227+
assertEquals(1, holdCount)
228+
assertTrue("Network still held for channel op", holdCount > 0)
229+
230+
// Channel op completes (release 2)
231+
holdCount--
232+
assertEquals(0, holdCount)
233+
assertFalse("All holds released", holdCount > 0)
234+
}
235+
236+
// ── HTLC Safety Margin ───────────────────────────────────────────
237+
238+
@Test
239+
fun `CLTV safety buffers provide adequate margin`() {
240+
// From rust-lightning channelmonitor.rs
241+
val maxBlocksForConf = 18 // ~3 hours
242+
val cltvClaimBuffer = maxBlocksForConf * 2 // 36 blocks, ~6 hours
243+
val latencyGracePeriod = 3 // ~30 minutes
244+
val htlcFailBackBuffer = cltvClaimBuffer + latencyGracePeriod // 39 blocks
245+
246+
assertEquals(36, cltvClaimBuffer)
247+
assertEquals(39, htlcFailBackBuffer)
248+
249+
// Burst sync worst case: 15 min interval + 2 min sync = 17 min
250+
// In blocks: ~1.7 blocks. Well within 36-block buffer.
251+
val burstIntervalMin = 15
252+
val burstSyncTimeoutMin = 2
253+
val worstCaseMinutes = burstIntervalMin + burstSyncTimeoutMin
254+
val worstCaseBlocks = worstCaseMinutes.toDouble() / 10 // ~10 min per block
255+
assertTrue("Burst worst case ($worstCaseBlocks blocks) within CLTV buffer ($cltvClaimBuffer blocks)",
256+
worstCaseBlocks < cltvClaimBuffer)
257+
}
258+
259+
@Test
260+
fun `chain polling interval sufficient for HTLC safety`() {
261+
// LDK-node polls bitcoind RPC every 2 seconds
262+
val chainPollingIntervalSecs = 2
263+
// Even if we miss a few polls, 36 blocks = ~360 minutes of buffer
264+
val cltvBufferMinutes = 36 * 10 // ~360 minutes
265+
assertTrue("2s polling is way faster than needed for $cltvBufferMinutes min buffer",
266+
chainPollingIntervalSecs < cltvBufferMinutes * 60)
267+
}
268+
}

0 commit comments

Comments
 (0)