Skip to content

Commit a3c3c65

Browse files
authored
4390: tests for memory leaks (#251)
1 parent 4e8b340 commit a3c3c65

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

services/blockassembly/subtreeprocessor/SubtreeProcessor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,10 @@ func (stp *SubtreeProcessor) reset(blockHeader *model.BlockHeader, moveBackBlock
835835
// clear current tx map
836836
stp.currentTxMap.Clear()
837837

838+
// clear remove map to prevent memory leak - entries for transactions that were
839+
// never dequeued would otherwise accumulate indefinitely across resets
840+
stp.removeMap = txmap.NewSwissMap(0)
841+
838842
// reset tx count
839843
stp.setTxCountFromSubtrees()
840844

services/blockassembly/subtreeprocessor/SubtreeProcessor_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2116,6 +2116,98 @@ func TestSubtreeProcessor_moveBackBlock(t *testing.T) {
21162116
}
21172117

21182118
func Test_removeMap(t *testing.T) {
2119+
t.Run("removeMap is cleared during reset - memory leak fix verification", func(t *testing.T) {
2120+
// This test verifies that removeMap entries ARE cleared during reset,
2121+
// preventing memory leaks from orphaned remove entries.
2122+
2123+
settings := test.CreateBaseTestSettings(t)
2124+
settings.BlockAssembly.InitialMerkleItemsPerSubtree = 128
2125+
2126+
newSubtreeChan := make(chan NewSubtreeRequest, 100)
2127+
done := make(chan struct{})
2128+
defer close(done)
2129+
2130+
go func() {
2131+
for {
2132+
select {
2133+
case newSubtreeRequest := <-newSubtreeChan:
2134+
if newSubtreeRequest.ErrChan != nil {
2135+
newSubtreeRequest.ErrChan <- nil
2136+
}
2137+
case <-done:
2138+
return
2139+
}
2140+
}
2141+
}()
2142+
2143+
// Create a mock blockchain client for reset operations
2144+
mockBlockchainClient := &blockchain.Mock{}
2145+
2146+
stp, err := NewSubtreeProcessor(t.Context(), ulogger.TestLogger{}, settings, nil, mockBlockchainClient, nil, newSubtreeChan)
2147+
require.NoError(t, err)
2148+
stp.Start(t.Context())
2149+
2150+
// Create some transaction hashes and add them ONLY to removeMap
2151+
// (not to the queue) - simulating a scenario where Remove() is called
2152+
// for transactions that were never queued
2153+
orphanedRemoveHashes := make([]chainhash.Hash, 50)
2154+
for i := 0; i < 50; i++ {
2155+
txHash := chainhash.HashH([]byte(fmt.Sprintf("orphaned-remove-tx-%d", i)))
2156+
orphanedRemoveHashes[i] = txHash
2157+
// Add directly to removeMap without ever queuing
2158+
err := stp.removeMap.Put(txHash)
2159+
require.NoError(t, err)
2160+
}
2161+
2162+
// Verify removeMap has entries before reset
2163+
removeMapLengthBeforeReset := stp.removeMap.Length()
2164+
assert.Equal(t, 50, removeMapLengthBeforeReset, "removeMap should have 50 entries before reset")
2165+
2166+
// Also add some transactions to currentTxMap to verify it IS cleared
2167+
for i := 0; i < 10; i++ {
2168+
txHash := chainhash.HashH([]byte(fmt.Sprintf("current-tx-%d", i)))
2169+
stp.currentTxMap.Set(txHash, subtreepkg.TxInpoints{})
2170+
}
2171+
currentTxMapLengthBeforeReset := stp.currentTxMap.Length()
2172+
assert.Equal(t, 10, currentTxMapLengthBeforeReset, "currentTxMap should have 10 entries before reset")
2173+
2174+
// Create a target header for reset
2175+
merkleRoot := chainhash.HashH([]byte("merkle"))
2176+
prevBlock := chainhash.HashH([]byte("prev"))
2177+
resetTargetHeader := &model.BlockHeader{
2178+
Version: 1,
2179+
HashMerkleRoot: &merkleRoot,
2180+
HashPrevBlock: &prevBlock,
2181+
Timestamp: 1234567890,
2182+
Nonce: 0,
2183+
}
2184+
2185+
// Initialize the current block header (required for reset)
2186+
stp.InitCurrentBlockHeader(resetTargetHeader)
2187+
2188+
// Perform reset with no moveBack or moveForward blocks
2189+
response := stp.Reset(resetTargetHeader, nil, nil, false, nil)
2190+
require.NoError(t, response.Err, "Reset should succeed")
2191+
2192+
// Verify currentTxMap WAS cleared (expected behavior)
2193+
currentTxMapLengthAfterReset := stp.currentTxMap.Length()
2194+
assert.Equal(t, 0, currentTxMapLengthAfterReset, "currentTxMap should be cleared after reset")
2195+
2196+
// Verify removeMap WAS cleared (fix for memory leak)
2197+
removeMapLengthAfterReset := stp.removeMap.Length()
2198+
assert.Equal(t, 0, removeMapLengthAfterReset,
2199+
"removeMap should be cleared after reset to prevent memory leak")
2200+
2201+
// Verify the specific orphaned hashes are NO LONGER in removeMap
2202+
for i, hash := range orphanedRemoveHashes {
2203+
exists := stp.removeMap.Exists(hash)
2204+
assert.False(t, exists, "Orphaned hash %d should NOT exist in removeMap after reset", i)
2205+
}
2206+
2207+
t.Logf("Memory leak fix verified: removeMap cleared from %d to %d entries after reset",
2208+
removeMapLengthBeforeReset, removeMapLengthAfterReset)
2209+
})
2210+
21192211
t.Run("when adding from queue", func(t *testing.T) {
21202212
settings := test.CreateBaseTestSettings(t)
21212213
settings.BlockAssembly.InitialMerkleItemsPerSubtree = 128
@@ -2173,6 +2265,112 @@ func Test_removeMap(t *testing.T) {
21732265
assert.Equal(t, uint64(expectedNrTransactions-transactionsRemoved+1), stp.TxCount()) //nolint:gosec // +1 for coinbase
21742266
assert.Equal(t, expectedNrTransactions-transactionsRemoved, stp.currentTxMap.Length())
21752267
})
2268+
2269+
t.Run("moveBackBlock adds to removeMap and reset clears it", func(t *testing.T) {
2270+
// This test verifies that when moveBackBlock processes coinbase child spends,
2271+
// entries are added to removeMap, and these entries are properly cleared during
2272+
// reset to prevent memory leaks.
2273+
2274+
ctx := context.Background()
2275+
utxoStoreURL, err := url.Parse("sqlitememory:///test")
2276+
require.NoError(t, err)
2277+
2278+
utxoStore, err := sql.New(ctx, ulogger.TestLogger{}, test.CreateBaseTestSettings(t), utxoStoreURL)
2279+
require.NoError(t, err)
2280+
require.NoError(t, utxoStore.SetBlockHeight(4))
2281+
2282+
blobStore := blob_memory.New()
2283+
settings := test.CreateBaseTestSettings(t)
2284+
settings.BlockAssembly.InitialMerkleItemsPerSubtree = 128
2285+
2286+
newSubtreeChan := make(chan NewSubtreeRequest, 10)
2287+
go func() {
2288+
for req := range newSubtreeChan {
2289+
if req.ErrChan != nil {
2290+
req.ErrChan <- nil
2291+
}
2292+
}
2293+
}()
2294+
defer close(newSubtreeChan)
2295+
2296+
stp, err := NewSubtreeProcessor(ctx, ulogger.TestLogger{}, settings, blobStore, nil, utxoStore, newSubtreeChan)
2297+
require.NoError(t, err)
2298+
stp.Start(ctx)
2299+
2300+
// Create coinbase transaction
2301+
coinbase := coinbaseTx
2302+
_, err = utxoStore.Create(ctx, coinbase, 1)
2303+
require.NoError(t, err)
2304+
2305+
// Create child transaction spending from coinbase
2306+
childTx := bt.NewTx()
2307+
err = childTx.From(coinbase.TxIDChainHash().String(), 0, coinbase.Outputs[0].LockingScript.String(), uint64(coinbase.Outputs[0].Satoshis))
2308+
require.NoError(t, err)
2309+
err = childTx.AddP2PKHOutputFromAddress("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", 400000000)
2310+
require.NoError(t, err)
2311+
childTx.Inputs[0].UnlockingScript = bscript.NewFromBytes([]byte{})
2312+
2313+
// Create child in UTXO store and establish parent-child relationship
2314+
_, err = utxoStore.Create(ctx, childTx, 1)
2315+
require.NoError(t, err)
2316+
spends, err := utxoStore.Spend(ctx, childTx, 2, utxo.IgnoreFlags{})
2317+
require.NoError(t, err)
2318+
for _, spend := range spends {
2319+
require.NoError(t, spend.Err)
2320+
}
2321+
2322+
childHash := *childTx.TxIDChainHash()
2323+
2324+
// Verify removeMap is empty before
2325+
removeMapLengthBefore := stp.removeMap.Length()
2326+
assert.Equal(t, 0, removeMapLengthBefore, "removeMap should be empty initially")
2327+
2328+
// Create block for moveBack
2329+
block := &model.Block{
2330+
CoinbaseTx: coinbase,
2331+
Header: &model.BlockHeader{
2332+
Version: 1,
2333+
HashPrevBlock: &chainhash.Hash{},
2334+
HashMerkleRoot: &chainhash.Hash{},
2335+
Timestamp: 1234567890,
2336+
Bits: model.NBit{},
2337+
Nonce: 12345,
2338+
},
2339+
Subtrees: []*chainhash.Hash{},
2340+
}
2341+
2342+
// Call removeCoinbaseUtxos directly (this is what moveBackBlock calls)
2343+
err = stp.removeCoinbaseUtxos(ctx, block)
2344+
require.NoError(t, err)
2345+
2346+
// Verify child hash was added to removeMap
2347+
removeMapLengthAfter := stp.removeMap.Length()
2348+
assert.Equal(t, 1, removeMapLengthAfter, "removeMap should have 1 entry after removeCoinbaseUtxos")
2349+
assert.True(t, stp.removeMap.Exists(childHash), "childHash should be in removeMap")
2350+
2351+
// Now verify that reset clears the removeMap entry
2352+
merkleRoot := chainhash.HashH([]byte("merkle"))
2353+
prevBlock := chainhash.HashH([]byte("prev"))
2354+
resetTargetHeader := &model.BlockHeader{
2355+
Version: 1,
2356+
HashMerkleRoot: &merkleRoot,
2357+
HashPrevBlock: &prevBlock,
2358+
Timestamp: 1234567890,
2359+
Nonce: 0,
2360+
}
2361+
stp.InitCurrentBlockHeader(resetTargetHeader)
2362+
2363+
// Reset with no moveBack/moveForward blocks
2364+
response := stp.Reset(resetTargetHeader, nil, nil, false, nil)
2365+
require.NoError(t, response.Err)
2366+
2367+
// Verify removeMap is cleared after reset (memory leak fix)
2368+
removeMapLengthAfterReset := stp.removeMap.Length()
2369+
assert.Equal(t, 0, removeMapLengthAfterReset,
2370+
"removeMap should be cleared after reset to prevent memory leak")
2371+
assert.False(t, stp.removeMap.Exists(childHash),
2372+
"childHash should be removed from removeMap after reset")
2373+
})
21762374
}
21772375

21782376
func waitForSubtreeProcessorQueueToEmpty(t *testing.T, stp *SubtreeProcessor) {

0 commit comments

Comments
 (0)