{
 "number": 35744,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35744",
 "title": "coins: prevent DB resize from invalidating cursors",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-07-17T22:25:59Z",
 "updated_at": "2026-09-16T20:46:05Z",
 "age_days": 61,
 "draft": false,
 "labels": [
  "UTXO Db and Indexes"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "38b84769608a0c17cd420da8760281471c656449",
 "head_ref": "l0rinc/coins-cursor-resize-lifetime",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-07-25T05:16:13Z",
   "sha": "1154f96c5fb6efd60d4d63ae0d75352632d95ef8"
  },
  {
   "t": "2026-07-28T01:15:08Z",
   "sha": "26a6abc4015371c7ee6d0164816c6fedd6e19c97"
  },
  {
   "t": "2026-07-28T02:39:42Z",
   "sha": "bd46208331e2bbcf92fa3f4e763d10fa44b7e8cf"
  },
  {
   "t": "2026-07-28T02:46:04Z",
   "sha": "38b84769608a0c17cd420da8760281471c656449"
  }
 ],
 "additions": 194,
 "deletions": 49,
 "changed_files": 9,
 "commit_count": 8,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "sedited",
      "url": "https://github.com/bitcoin/bitcoin/pull/35744#issuecomment-5588425476"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36167,
     "title": "[RFC] Enable `-Wunused`",
     "author": "fanquake"
    },
    {
     "number": 35713,
     "title": "Remove boost as a unit test runner",
     "author": "rustaceanrob"
    },
    {
     "number": 29491,
     "title": "[EXPERIMENTAL] Schnorr batch verification for blocks",
     "author": "fjahr"
    }
   ]
  }
 },
 "acks_parsed": {
  "sedited": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-09-08T16:25:47Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "andrewtoth",
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-07-28T02:46:04Z",
  "last_reviewer_activity": "2026-09-08T16:25:47Z",
  "last_reviewer": "sedited",
  "author_silent_days": 51,
  "waiting_on_author_days": 8,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   35465
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35465,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-06-22",
    "title": "coins: compact chainstate regularly"
   }
  ],
  "conflicts": [
   36167,
   35713,
   29491
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/txdb.cpp",
  "src/txdb.h"
 ],
 "body": "**Problem:** `gettxoutsetinfo`, `scantxoutset`, and `dumptxoutset` retain LevelDB iterators after releasing `cs_main`.\nWhile those iterators are live, AssumeUTXO cache rebalancing can call `ResizeCache()`, replace `m_db`, and cause LevelDB to abort.\nThe issue was [reported during review of #35465](https://github.com/bitcoin/bitcoin/pull/35465#discussion_r3428902672).\n\n**Fix:** Each cursor holds a shared lock on `m_db_mutex` for its lifetime.\nCompaction takes the same shared lock, allowing it to run alongside cursors.\n`ResizeCache()` takes `m_db_mutex` exclusively, so it waits for their shared locks to be released before replacing `m_db`.\n`ResizeCache()` holds `cs_main` while it waits, so a long-running UTXO scan can stall validation during cache rebalancing.\n\nHistorical test failures without the fix\n\n```text\ntest/coins_tests.cpp:1090: error: in \"coins_tests/coins_db_cursor_resize\": check !CCoinsViewDBTestAccess::TryExclusiveLock(db) has failed\ntest/coins_tests.cpp:1102: error: in \"coins_tests/coins_db_cursor_resize\": check status == std::future_status::timeout has failed [0 != 1]\n```",
 "commits": [
  {
   "sha": "49d961776e2b4ea7720810dada44f15bab9c2ec5",
   "date": "2026-07-27T19:20:07Z",
   "message": "refactor: extract common lock acquisition\n\nMove lock acquisition and contention logging into `EnterLock()` so exclusive and shared lock wrappers use the same path.\nThis keeps the shared-locking commit focused on its new behavior."
  },
  {
   "sha": "4d25c38ad5db8001a9ecee94b2acc73760e5225c",
   "date": "2026-07-28T01:06:00Z",
   "message": "sync: add shared mutex lock checking\n\nRaw `std::shared_mutex` users bypass `DEBUG_LOCKORDER`, lock-contention logging, and Clang thread-safety analysis.\n\nAdd `SharedMutex`, `SharedLock`, and `READ_LOCK` alongside the existing exclusive-lock wrappers.\nThe test covers exclusive writes, shared reads, and the lock assertions.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "852a9f0901918a3bf23e4666578737153cf84f4d",
   "date": "2026-07-28T01:06:00Z",
   "message": "sync: support non-LIFO lock release\n\nLock tracking always removes the latest entry, but a long-lived shared lock can outlive an outer lock.\nReleasing the outer lock would remove the shared lock and corrupt `DEBUG_LOCKORDER` state.\n\nIdentify the released mutex and erase its latest entry.\nErasing a middle entry requires `CLockLocation` to be move-assignable, so make `m_thread_name` non-const.\nThe test releases an outer lock while a shared lock remains live, then verifies that the inverse order is detected and the stack is emptied.\n\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "264766d5809bbf5c3d1a60ec2a52eb7ad37112ed",
   "date": "2026-07-28T01:06:00Z",
   "message": "script: check signature cache shared locking\n\nUse `SharedMutex`, `READ_LOCK`, and `LOCK` so `DEBUG_LOCKORDER`, lock-contention diagnostics, and Clang thread-safety analysis cover signature-cache access.\nMark `setValid` `GUARDED_BY(cs_sigcache)` to require locking at access sites."
  },
  {
   "sha": "e638704c6f166d9111b9846bd55840077fec7f57",
   "date": "2026-07-28T01:06:00Z",
   "message": "test: check cuckoo cache shared locking\n\nUse the checked shared-mutex wrappers in the parallel cuckoo-cache test so lock-order and contention diagnostics cover this existing shared-lock user."
  },
  {
   "sha": "1678b11ff29b136579d7af334cd3f656a0618fda",
   "date": "2026-07-28T02:27:20Z",
   "message": "test: characterize coins DB cursor lifetime\n\n`ResizeCache()` currently returns while a cursor remains live, and full compaction runs concurrently with cursors.\n\nKeep the underlying `CDBWrapper` alive and use a separate path for replacement so the resize behavior can be observed without hitting the LevelDB live-iterator assertion.\nKeep a cursor live during compaction to characterize the existing behavior there too."
  },
  {
   "sha": "05c0e6cb6f99742cbf3fc8693b2329e23d66a84d",
   "date": "2026-07-28T02:43:37Z",
   "message": "coins: block DB resize during cursor iteration\n\n`gettxoutsetinfo`, `scantxoutset`, and `dumptxoutset` retain LevelDB iterators after releasing `cs_main`.\nAssumeUTXO cache rebalancing can then call `ResizeCache()` and replace `m_db` while an iterator is live.\nThe issue was reported in https://github.com/bitcoin/bitcoin/pull/35465#discussion_r3428902672.\n\nEach cursor holds a shared lock on `m_db_mutex` for its lifetime, while `ResizeCache()` takes the lock exclusively before replacing `m_db`.\nFull compaction remains exclusive in this commit, so it waits for every live cursor to be destroyed.\n\nCurrent callers keep each cursor on its creating thread, as required by `std::shared_mutex` and LevelDB iterators.\n\nCo-authored-by: Pieter Wuille <pieter@wuille.net>\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  },
  {
   "sha": "38b84769608a0c17cd420da8760281471c656449",
   "date": "2026-07-28T02:43:37Z",
   "message": "coins: allow DB cursors during compaction\n\nFull compaction must prevent `ResizeCache()` from replacing `m_db`, but it can run alongside cursors.\n\nTake `m_db_mutex` shared during compaction.\n`ResizeCache()` still takes it exclusively and waits for both compaction and live cursors before replacing `m_db`.\nAnnotate `m_compaction` with its existing `cs_main` guard.\n\nThe regression test keeps a cursor live and requires compaction to finish before releasing it.\n\nCo-authored-by: Pieter Wuille <pieter@wuille.net>\nCo-authored-by: Andrew Toth <andrewstoth@gmail.com>"
  }
 ],
 "timeline": [
  {
   "t": "2026-07-18T02:31:03Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/txdb.h",
   "commit": "d3484251b3fc964c7d2f3c02bc72fb33d5f099a1",
   "in_reply_to": null,
   "text": "Thanks for tackling this.\n\nI'm not sure about this approach with an atomic counter though. Would it make sense to instead have a shared mutex that would allow multiple consumers to take a read lock along with the cursor, which are then released in the destructor? Then only `ResizeCache` takes an exclusive lock? Just brainstorming here."
  },
  {
   "t": "2026-07-18T04:45:24Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/txdb.h",
   "commit": "d3484251b3fc964c7d2f3c02bc72fb33d5f099a1",
   "in_reply_to": 3607375687,
   "text": "Thanks, I already investigated this, but the warnings and LLM reviews scared me off because it seems to force cursors to be destroyed on the thread that created them: `unlock_shared()` requires [the calling thread to hold the shared lock](https://eel.is/c++draft/thread.sharedmutex.requirements#12). Current callers seem to satisfy that, though, so maybe it is fine.\nI also found that `std::shared_mutex` may lose some of Core's lock checking: [this Godbolt recreates it](https://godbolt.org/z/3q94MYEsb), which still warns inside a `std::unique_lock<std::shared_mutex>`, and my local `DEBUG_LOCKORDER` probe did not see the lock.\nWhat do you think?"
  },
  {
   "t": "2026-07-18T21:41:04Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/txdb.h",
   "commit": "d3484251b3fc964c7d2f3c02bc72fb33d5f099a1",
   "in_reply_to": 3607375687,
   "text": "[quoted text omitted]\n\n[Iterators alone are not thread-safe](https://github.com/bitcoin-core/leveldb-subtree/blob/bitcoin-fork/doc/index.md#concurrency) though, so we should not be passing them to other threads. I think in this case it should be fine.\n\n[quoted text omitted]\nDo we need this here? We do use a `shared_mutex` already in `SignatureCache` without any of the lock checking machinery."
  },
  {
   "t": "2026-07-25T05:16:13Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "1154f96c5fb6efd60d4d63ae0d75352632d95ef8"
  },
  {
   "t": "2026-07-25T05:28:02Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks, took your suggestions, @andrewtoth.\nI reworked the fix around it and split the series into a characterization test, a commit that makes `ResizeCache()` wait for live cursors, and a follow-up that lets cursors remain live during compaction.\nLet me know if you think it's better this way."
  },
  {
   "t": "2026-07-26T18:02:18Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/txdb.cpp",
   "commit": "1154f96c5fb6efd60d4d63ae0d75352632d95ef8",
   "in_reply_to": null,
   "text": "Should we add `AssertLockHeld(::cs_main);` here?"
  },
  {
   "t": "2026-07-26T18:30:29Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Thanks, I do like this better.\nWhat do you think of adding shared locking to our lock checking machinery?\nSince we now take a shared lock while inside a separate lock which is released while the shared lock continues to be held, it would require us to make a modification to make sure we're popping the correct lock being released. This would let us catch issues with DEBUG_LOCKORDER.\n\nSomething like this?\n\n```diff\ndiff --git a/src/sync.cpp b/src/sync.cpp\nindex 6d740866d4..838f420c96 100644\n--- a/src/sync.cpp\n+++ b/src/sync.cpp\n@@ -14,6 +14,7 @@\n #include <map>\n #include <mutex>\n #include <set>\n+#include <shared_mutex>\n #include <system_error>\n #include <thread>\n #include <type_traits>\n@@ -31,6 +32,7 @@ void ContendedLock(std::string_view name, std::string_view file, int nLine, Lock\n }\n template void ContendedLock(std::string_view name, std::string_view file, int nLine, std::unique_lock<std::mutex>& lock);\n template void ContendedLock(std::string_view name, std::string_view file, int nLine, std::unique_lock<std::recursive_mutex>& lock);\n+template void ContendedLock(std::string_view name, std::string_view file, int nLine, std::unique_lock<std::shared_mutex>& lock);\n\n #endif\n\n@@ -75,7 +77,7 @@ private:\n     bool fTry;\n     std::string mutexName;\n     std::string sourceFile;\n-    const std::string m_thread_name;\n+    std::string m_thread_name;\n     int sourceLine;\n };\n\n@@ -204,13 +206,20 @@ static void push_lock(MutexType* c, const CLockLocation& locklocation)\n     }\n }\n\n-static void pop_lock()\n+static void pop_lock(void* cs)\n {\n     LockData& lockdata = GetLockData();\n     STDLOCK(lockdata.dd_mutex);\n\n     LockStack& lock_stack = lockdata.m_lock_stacks[std::this_thread::get_id()];\n-    lock_stack.pop_back();\n+    // Erase the most recent entry for this mutex rather than popping the back:\n+    // locks that outlive their creating scope may be released out of LIFO order.\n+    for (auto it = lock_stack.rbegin(); it != lock_stack.rend(); ++it) {\n+        if (it->first == cs) {\n+            lock_stack.erase(std::next(it).base());\n+            break;\n+        }\n+    }\n     if (lock_stack.empty()) {\n         lockdata.m_lock_stacks.erase(std::this_thread::get_id());\n     }\n@@ -223,6 +232,7 @@ void EnterCritical(const char* pszName, const char* pszFile, int nLine, MutexTyp\n }\n template void EnterCritical(const char*, const char*, int, std::mutex*, bool);\n template void EnterCritical(const char*, const char*, int, std::recursive_mutex*, bool);\n+template void EnterCritical(const char*, const char*, int, std::shared_mutex*, bool);\n\n void CheckLastCritical(void* cs, std::string& lockname, const char* guardname, const char* file, int line)\n {\n@@ -250,9 +260,9 @@ void CheckLastCritical(void* cs, std::string& lockname, const char* guardname, c\n     throw std::logic_error(strprintf(\"%s was not most recent critical section locked\", guardname));\n }\n\n-void LeaveCritical()\n+void LeaveCritical(void* cs)\n {\n-    pop_lock();\n+    pop_lock(cs);\n }\n\n static std::string LocksHeld()\ndiff --git a/src/sync.h b/src/sync.h\nindex 28bc78e911..fcb870ecaf 100644\n--- a/src/sync.h\n+++ b/src/sync.h\n@@ -14,6 +14,7 @@\n #include <cassert>\n #include <condition_variable>\n #include <mutex>\n+#include <shared_mutex>\n #include <string>\n #include <thread>\n\n@@ -47,7 +48,7 @@ TRY_LOCK(mutex, name);\n #ifdef DEBUG_LOCKORDER\n template <typename MutexType>\n void EnterCritical(const char* pszName, const char* pszFile, int nLine, MutexType* cs, bool fTry = false);\n-void LeaveCritical();\n+void LeaveCritical(void* cs);\n void CheckLastCritical(void* cs, std::string& lockname, const char* guardname, const char* file, int line);\n template <typename MutexType>\n void AssertLockHeldInternal(const char* pszName, const char* pszFile, int nLine, MutexType* cs) EXCLUSIVE_LOCKS_REQUIRED(cs);\n@@ -65,7 +66,7 @@ extern bool g_debug_lockorder_abort;\n #else\n template <typename MutexType>\n inline void EnterCritical(const char* pszName, const char* pszFile, int nLine, MutexType* cs, bool fTry = false) {}\n-inline void LeaveCritical() {}\n+inline void LeaveCritical(void* cs) {}\n inline void CheckLastCritical(void* cs, std::string& lockname, const char* guardname, const char* file, int line) {}\n template <typename MutexType>\n inline void AssertLockHeldInternal(const char* pszName, const char* pszFile, int nLine, MutexType* cs) EXCLUSIVE_LOCKS_REQUIRED(cs) {}\n@@ -113,6 +114,7 @@ public:\n     }\n\n     using unique_lock = std::unique_lock<PARENT>;\n+    using shared_lock = std::shared_lock<PARENT>;\n #ifdef __clang__\n     //! For negative capabilities in the Clang Thread Safety Analysis.\n     //! A negative requirement uses the EXCLUSIVE_LOCKS_REQUIRED attribute, in conjunction\n@@ -130,6 +132,9 @@ using RecursiveMutex = AnnotatedMixin<std::recursive_mutex>;\n /** Wrapped mutex: supports waiting but not recursive locking */\n using Mutex = AnnotatedMixin<std::mutex>;\n\n+/** Wrapped shared mutex: supports read locking via SharedLock and exclusive locking via LOCK */\n+using SharedMutex = AnnotatedMixin<std::shared_mutex>;\n+\n /** Different type to mark Mutex at global scope\n  *\n  * Thread safety analysis can't handle negative assertions about mutexes\n@@ -173,7 +178,7 @@ private:\n         if (Base::try_lock()) {\n             return true;\n         }\n-        LeaveCritical();\n+        LeaveCritical(Base::mutex());\n         return false;\n     }\n\n@@ -200,7 +205,7 @@ public:\n     ~UniqueLock() UNLOCK_FUNCTION()\n     {\n         if (Base::owns_lock())\n-            LeaveCritical();\n+            LeaveCritical(Base::mutex());\n     }\n\n     operator bool()\n@@ -224,7 +229,7 @@ public:\n\n             CheckLastCritical((void*)lock.mutex(), lockname, _guardname, _file, _line);\n             lock.unlock();\n-            LeaveCritical();\n+            LeaveCritical(lock.mutex());\n             lock.swap(templock);\n         }\n\n@@ -253,6 +258,34 @@ public:\n // the sake of thread-safety analysis, but it is not actually used otherwise.\n #define REVERSE_LOCK(g, cs) typename std::decay<decltype(g)>::type::reverse_lock BITCOIN_UNIQUE_NAME(revlock)(g, cs, #cs, __FILE__, __LINE__)\n\n+/**\n+ * An RAII shared (read) lock for SharedMutex that registers with the\n+ * DEBUG_LOCKORDER checker. Unlike LOCK(), an instance may outlive the scope\n+ * that created it, but it must be destroyed on the thread that created it.\n+ */\n+template <typename MutexType>\n+class SharedLock : public MutexType::shared_lock\n+{\n+    using Base = typename MutexType::shared_lock;\n+\n+public:\n+    SharedLock(MutexType& mutex, const char* name, const char* file, int line)\n+        : Base{mutex, std::defer_lock}\n+    {\n+        EnterCritical(name, file, line, Base::mutex());\n+        Base::lock();\n+    }\n+\n+    SharedLock(SharedLock&&) = default;\n+\n+    ~SharedLock()\n+    {\n+        if (Base::owns_lock()) LeaveCritical(Base::mutex());\n+    }\n+};\n+\n+#define READ_LOCK(cs) SharedLock BITCOIN_UNIQUE_NAME(sharedlock)(cs, #cs, __FILE__, __LINE__)\n+\n // When locking a Mutex, require negative capability to ensure the lock\n // is not already held\n inline Mutex& MaybeCheckNotHeld(Mutex& cs) EXCLUSIVE_LOCKS_REQUIRED(!cs) LOCK_RETURNED(cs) { return cs; }\n\n```"
  },
  {
   "t": "2026-07-28T01:15:08Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "26a6abc4015371c7ee6d0164816c6fedd6e19c97"
  },
  {
   "t": "2026-07-28T01:27:40Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks, I implemented both suggestions and pushed the reworked series.\n\nThe rework is quite heavy and needs a thorough review, but I think I managed to split it into reviewable logical chunks: common lock acquisition, checked shared-lock support, non-LIFO lock tracking, migration of `SignatureCache` and the parallel cuckoo-cache test, then characterization and fixes for cursor lifetime during resize, destruction, and compaction.\n\n`ResizeCache()` now asserts `cs_main`, cursors and compaction hold `m_db_mutex` shared, while resize takes it exclusively.\n\nEdit: looks like a member mutex cannot guard its own destruction - TSAN should be fixed."
  },
  {
   "t": "2026-07-28T02:39:42Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "bd46208331e2bbcf92fa3f4e763d10fa44b7e8cf"
  },
  {
   "t": "2026-07-28T02:46:04Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "38b84769608a0c17cd420da8760281471c656449"
  },
  {
   "t": "2026-09-08T16:25:47Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Concept ACK"
  }
 ],
 "labels_log": [
  {
   "t": "2026-07-17T22:26:02Z",
   "action": "labeled",
   "label": "UTXO Db and Indexes",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-28T02:18:57Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-28T03:49:23Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-07-28T01:55:35Z",
   "kind": "convert_to_draft",
   "who": "l0rinc"
  },
  {
   "t": "2026-07-28T02:46:31Z",
   "kind": "ready_for_review",
   "who": "l0rinc"
  }
 ],
 "text_chars": 14121,
 "text_tokens_estimate": 3530,
 "changed_paths": [
  "src/script/sigcache.cpp",
  "src/script/sigcache.h",
  "src/sync.cpp",
  "src/sync.h",
  "src/test/coins_tests.cpp",
  "src/test/cuckoocache_tests.cpp",
  "src/test/sync_tests.cpp",
  "src/txdb.cpp",
  "src/txdb.h"
 ],
 "files": [
  {
   "path": "src/script/sigcache.cpp",
   "add": 2,
   "del": 4
  },
  {
   "path": "src/script/sigcache.h",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/sync.cpp",
   "add": 17,
   "del": 5
  },
  {
   "path": "src/sync.h",
   "add": 51,
   "del": 15
  },
  {
   "path": "src/test/coins_tests.cpp",
   "add": 52,
   "del": 3
  },
  {
   "path": "src/test/cuckoocache_tests.cpp",
   "add": 5,
   "del": 6
  },
  {
   "path": "src/test/sync_tests.cpp",
   "add": 48,
   "del": 0
  },
  {
   "path": "src/txdb.cpp",
   "add": 9,
   "del": 7
  },
  {
   "path": "src/txdb.h",
   "add": 7,
   "del": 6
  }
 ],
 "test_lines": 114,
 "git": {
  "head": "38b84769608a0c17cd420da8760281471c656449",
  "head_matches_backup": true,
  "base": "7dea464d6b51a69bd99a0451be8aaf3a26313eb6",
  "commits": [
   {
    "sha": "49d961776e",
    "subject": "refactor: extract common lock acquisition",
    "files": 1,
    "add": 16,
    "del": 9
   },
   {
    "sha": "4d25c38ad5",
    "subject": "sync: add shared mutex lock checking",
    "files": 3,
    "add": 56,
    "del": 1
   },
   {
    "sha": "852a9f0901",
    "subject": "sync: support non-LIFO lock release",
    "files": 3,
    "add": 45,
    "del": 11
   },
   {
    "sha": "264766d580",
    "subject": "script: check signature cache shared locking",
    "files": 2,
    "add": 5,
    "del": 7
   },
   {
    "sha": "e638704c6f",
    "subject": "test: check cuckoo cache shared locking",
    "files": 1,
    "add": 5,
    "del": 6
   },
   {
    "sha": "1678b11ff2",
    "subject": "test: characterize coins DB cursor lifetime",
    "files": 2,
    "add": 54,
    "del": 3
   },
   {
    "sha": "05c0e6cb6f",
    "subject": "coins: block DB resize during cursor iteration",
    "files": 3,
    "add": 16,
    "del": 14
   },
   {
    "sha": "38b8476960",
    "subject": "coins: allow DB cursors during compaction",
    "files": 3,
    "add": 5,
    "del": 6
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "44515057d5dabe6c",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}