@@ -729,26 +729,72 @@ void NodeImpl::run_think()
729729 [this , result = std::move (result)]()
730730 {
731731 // Trim stale shares (on ioc thread — safe with phase2)
732+ // Python p2pool pattern (node.py:355-398):
733+ // 1. Collect hashes to evict from chain (dry run)
734+ // 2. Remove from verified FIRST (borrows share data from chain)
735+ // 3. Remove from chain (frees share data)
736+ // This prevents use-after-free: verified must release its
737+ // borrowed references before chain destroys the share data.
732738 const size_t keep_per_head = PoolConfig::chain_length () * 2 + 10 ;
733739 std::vector<uint256> evicted_from_chain;
734- auto trim_chain = [&](auto & sc, const char * label, bool owns_data = true ,
735- std::vector<uint256>* evicted = nullptr ) {
736- if (sc.size () <= keep_per_head)
737- return ;
738- auto heads_copy = sc.get_heads ();
740+ std::vector<ltc::ShareType> deferred_shares;
741+
742+ // Step 1: Trim chain, deferring share destruction
743+ // Share data is moved to deferred_shares instead of being freed,
744+ // because verified may still hold borrowed references to it.
745+ if (m_tracker.chain .size () > keep_per_head)
746+ {
747+ auto heads_copy = m_tracker.chain .get_heads ();
739748 size_t total_removed = 0 ;
740749 for (auto & [head_hash, tail_hash] : heads_copy) {
741- auto removed = sc.trim (head_hash, keep_per_head, owns_data, evicted);
750+ auto removed = m_tracker.chain .trim (head_hash, keep_per_head,
751+ /* owns_data=*/ true , &evicted_from_chain, &deferred_shares);
742752 total_removed += removed;
743753 }
744754 if (total_removed > 0 )
745- LOG_INFO << " [Pool] Trimmed " << total_removed << " old shares from " << label
746- << " (now " << sc.size () << " )" ;
747- };
748- trim_chain (m_tracker.verified , " verified" , /* owns_data=*/ false );
749- trim_chain (m_tracker.chain , " chain" , /* owns_data=*/ true , &evicted_from_chain);
755+ LOG_INFO << " [Pool] Trimmed " << total_removed
756+ << " old shares from chain (now " << m_tracker.chain .size () << " )" ;
757+ }
758+
759+ // Step 2: Cascade — remove evicted shares from verified BEFORE
760+ // destroying share data (Python p2pool pattern: node.py:396-398)
761+ if (!evicted_from_chain.empty ())
762+ {
763+ size_t cascade_removed = 0 ;
764+ for (const auto & h : evicted_from_chain)
765+ {
766+ if (m_tracker.verified .contains (h))
767+ {
768+ m_tracker.verified .remove (h, /* owns_data=*/ false );
769+ ++cascade_removed;
770+ }
771+ }
772+ if (cascade_removed > 0 )
773+ LOG_INFO << " [Pool] Cascaded " << cascade_removed
774+ << " evictions to verified (now " << m_tracker.verified .size () << " )" ;
775+ }
776+
777+ // Step 3: Now safe to destroy deferred share data
778+ for (auto & share : deferred_shares)
779+ share.destroy ();
780+ deferred_shares.clear ();
781+
782+ // Step 4: Also trim verified independently (may have excess)
783+ if (m_tracker.verified .size () > keep_per_head)
784+ {
785+ auto heads_copy = m_tracker.verified .get_heads ();
786+ size_t total_removed = 0 ;
787+ for (auto & [head_hash, tail_hash] : heads_copy) {
788+ auto removed = m_tracker.verified .trim (head_hash, keep_per_head,
789+ /* owns_data=*/ false );
790+ total_removed += removed;
791+ }
792+ if (total_removed > 0 )
793+ LOG_INFO << " [Pool] Trimmed " << total_removed
794+ << " old shares from verified (now " << m_tracker.verified .size () << " )" ;
795+ }
750796
751- // Prune evicted shares from LevelDB
797+ // Step 5: Prune evicted shares from LevelDB
752798 if (!evicted_from_chain.empty () && m_storage && m_storage->is_available ())
753799 {
754800 size_t pruned = 0 ;
0 commit comments