{
 "number": 36180,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36180",
 "title": "qt: don't abort or drop settings when dynamic settings are disabled",
 "author": "jeanpablojp",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-09-07T02:10:31Z",
 "updated_at": "2026-10-27T22:07:57Z",
 "description_hash": "a04abd2401b9d743",
 "age_days": 55,
 "draft": false,
 "labels": [
  "GUI"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "7ff7e9bbe5fbfee162d55de96c79954ead2c24d5",
 "head_ref": "bitcoin-qt-nosettings-guard",
 "head_repo": "jeanpablojp/bitcoin",
 "head_history": [],
 "additions": 108,
 "deletions": 27,
 "changed_files": 7,
 "commit_count": 2,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "sedited"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-27T22:07:57Z",
  "last_reviewer_activity": "2026-10-19T18:05:53Z",
  "last_reviewer": "sedited",
  "author_silent_days": 5,
  "waiting_on_author_days": 0,
  "days_since_update": 5
 },
 "refs": {
  "mentioned": [
   36176
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36176,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-07",
    "title": "wallet: avoid a crash when creating a wallet with -nosettings"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "`ArgsManager::WriteSettingsFile` throws when dynamic settings are disabled, and three call sites in `NodeImpl` call it without checking first. So `bitcoin-qt -nosettings` aborts on `-resetguisettings`, and on the legacy GUI settings migration when an old Qt setting is still present, which comes back on every start because the abort happens before the key is removed.\n\nThe fourth call site in that file is the wallet path in `ChainImpl`, and #36176 guards it. These three are the options dialog, and I ran into them while testing that PR.\n\nThe flag is easier to reach than it looks, since the fatal error box suggests `-nosettings` when the settings file cannot be written.\n\nGuarding the writes on its own is not enough, because three things then happen silently instead of aborting. The migration drops the legacy keys, since it removes them whether or not the write landed. The dialog closes reporting success, since `setOption` only reported parse errors and `QDataWidgetMapper::submit()` does not propagate what `setData` returns. And it asks for a restart that would discard the change rather than apply it, which is reachable today without the flag.\n\n`bitcoin-qt -regtest -nosettings -resetguisettings` aborts on master and starts here. The Qt tests cover the guards and each of those, with settings disabled and with a write that fails, and each one fails without its change.\n\nThe second commit reports rather than fixes the abort, so it can be dropped if reviewers would rather keep this to the crash.",
 "commits": [
  {
   "sha": "cfd09ec29569f194312b1ea8c619a793e1ffc009",
   "date": "2026-09-06T21:30:26Z",
   "message": "node, qt: don't write settings.json when dynamic settings are disabled\n\nArgsManager::WriteSettingsFile throws when dynamic settings are disabled\nwith -nosettings, and three call sites in NodeImpl call it unguarded, so\nbitcoin-qt -nosettings aborts on -resetguisettings and on the legacy GUI\nsettings migration. The migration case comes back on every start,\nbecause the abort happens before the legacy key is removed.\n\nGuard the writes, and skip the migration when there is nowhere to\nmigrate to, so legacy Qt settings are left in place rather than silently\ndropped."
  },
  {
   "sha": "7ff7e9bbe5fbfee162d55de96c79954ead2c24d5",
   "date": "2026-09-07T00:59:09Z",
   "message": "qt: report a settings write that did not reach disk\n\nOptionsModel::setOption only reported failure on a parse error, so a\nwrite that never landed closed the dialog with no sign of it, and\nQDataWidgetMapper::submit() does not propagate what setData returns, so\nreading its result would not have helped. setRestartRequired() was\ncalled the same way, so the user was also told to restart, and the\nrestart would drop the in-memory change rather than apply it.\n\nRecord a failed write on the model, warn once the dialog has submitted,\nand only ask for a restart when the writes succeeded."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-19T18:05:53Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Is this relevant for the qml gui too? cc @johnny9",
   "id": "c:6273887938",
   "url": "https://github.com/bitcoin/bitcoin/pull/36180#issuecomment-6273887938",
   "hash": "c62cb2117b4f5d7f",
   "edited": null
  },
  {
   "t": "2026-10-27T22:07:57Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "From reading gui-qml, partly. It gets its node from `init->makeNode()` and calls `updateRwSetting` in `UpdateRwSetting` and when saving the default receive address type, so it gets the `NodeImpl` half, and under `-nosettings` those calls return false instead of throwing.\n\nThe `OptionsModel` half is bitcoin-qt only. Note that gui-qml's `StubNode` and `MockNode` override `updateRwSetting` returning `void`, so its tests won't compile against a submodule with this until those overrides return `bool`.",
   "id": "c:6454809073",
   "url": "https://github.com/bitcoin/bitcoin/pull/36180#issuecomment-6454809073",
   "hash": "8aae0ed093c55e0f",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-07T02:10:36Z",
   "action": "labeled",
   "label": "GUI",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 3278,
 "text_tokens_estimate": 819,
 "changed_paths": [
  "src/interfaces/node.h",
  "src/node/interfaces.cpp",
  "src/qt/optionsdialog.cpp",
  "src/qt/optionsmodel.cpp",
  "src/qt/optionsmodel.h",
  "src/qt/test/optiontests.cpp",
  "src/qt/test/optiontests.h"
 ],
 "files": [
  {
   "path": "src/interfaces/node.h",
   "add": 6,
   "del": 2
  },
  {
   "path": "src/node/interfaces.cpp",
   "add": 4,
   "del": 4
  },
  {
   "path": "src/qt/optionsdialog.cpp",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/qt/optionsmodel.cpp",
   "add": 34,
   "del": 21
  },
  {
   "path": "src/qt/optionsmodel.h",
   "add": 7,
   "del": 0
  },
  {
   "path": "src/qt/test/optiontests.cpp",
   "add": 51,
   "del": 0
  },
  {
   "path": "src/qt/test/optiontests.h",
   "add": 2,
   "del": 0
  }
 ],
 "test_lines": 53,
 "git": {
  "head": "7ff7e9bbe5fbfee162d55de96c79954ead2c24d5",
  "head_matches_backup": true,
  "base": "a174dd4017cf97f283b1848bb31998bfa28465e9",
  "patch_id": "959c9f52645842ef",
  "commits": [
   {
    "sha": "cfd09ec295",
    "subject": "node, qt: don't write settings.json when dynamic settings are disabled",
    "files": 5,
    "add": 34,
    "del": 4
   },
   {
    "sha": "7ff7e9bbe5",
    "subject": "qt: report a settings write that did not reach disk",
    "files": 7,
    "add": 79,
    "del": 28
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "959c9f52645842ef",
 "input_hash": "e6c5581e4308f446",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}