@@ -2116,6 +2116,98 @@ func TestSubtreeProcessor_moveBackBlock(t *testing.T) {
21162116}
21172117
21182118func 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
21782376func waitForSubtreeProcessorQueueToEmpty (t * testing.T , stp * SubtreeProcessor ) {
0 commit comments