{
 "number": 36293,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36293",
 "title": "contrib: exclude inactive keys from the binary quorum by default",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-09-17T23:21:08Z",
 "updated_at": "2026-10-04T14:51:59Z",
 "description_hash": "88c0ca4731491e90",
 "age_days": 45,
 "draft": false,
 "labels": [
  "Scripts and tools"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
 "head_ref": "l0rinc/verify-binaries-inactive-signers",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-09-18T21:46:03Z",
   "sha": "b956ee67ffbd2700b8f2be0bf63a2bdc7f31170a"
  },
  {
   "t": "2026-09-24T00:40:14Z",
   "sha": "73534a2407bbb8d72af33ebad7cb6d59a348af23"
  },
  {
   "t": "2026-09-25T01:01:37Z",
   "sha": "034271d4e1739347a31c64dbc4bfef8aa133ad1d"
  }
 ],
 "additions": 125,
 "deletions": 15,
 "changed_files": 3,
 "commit_count": 5,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "willcl-ark",
      "url": "https://github.com/bitcoin/bitcoin/pull/36293#pullrequestreview-5315970979"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "willcl-ark": {
   "kind": "ack",
   "hash": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "t": "2026-09-25T09:31:25Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "sedited",
   "willcl-ark"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-25T01:11:55Z",
  "last_reviewer_activity": "2026-09-25T09:31:25Z",
  "last_reviewer": "willcl-ark",
  "author_silent_days": 37,
  "waiting_on_author_days": 37,
  "days_since_update": 28
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "contrib/verify-binaries/verify.py"
 ],
 "body": "**Problem:** The binary verifier counts signatures from expired and revoked keys toward `--min-good-sigs`.\nEnough of these signatures can satisfy the threshold without any active signing key, and they are returned as good signatures.\n\n**Fix:** Warn about expired and revoked signatures and exclude them from threshold counting by default.\nFor historical verification, expired signatures can count toward the threshold with `--allow-expired` or `BINVERIFY_ALLOW_EXPIRED`, including signatures created after key expiry. Revoked signatures remain excluded.\nExpired signatures are reported separately, including in the `expired_sigs` field in successful JSON output.\n\nThe decision uses GnuPG\u2019s current key status from the local keyring, even if the signature was created while the key was active.\n\nThis was found and disclosed responsibly by the Red Team \ud83d\udfe5.",
 "commits": [
  {
   "sha": "431cac72734c267e9722f31667a05c7ce8c6ae91",
   "date": "2026-09-24T23:36:57Z",
   "message": "test: characterize inactive signature quorum\n\nExtract the parser for tests and add an empty expired-signature result to keep the characterization's return shape stable through the fixes.\n\nRecord that expired and revoked signatures currently count toward the quorum and can appear among good signatures."
  },
  {
   "sha": "20e15a5fdc5dea8ffc18c91a0d088dba8de2215c",
   "date": "2026-09-24T23:37:04Z",
   "message": "contrib: exclude revoked keys from binary quorum\n\nRevoked signatures currently count toward the quorum and can appear among good signatures.\nExclude them and give revocation precedence when GnuPG reports both statuses for a key.\n\nCo-authored-by: Rob Hamilton <6456095+Rob1Ham@users.noreply.github.com>"
  },
  {
   "sha": "02939e424d3834c1db94c3fbd8ce731ec6e28e41",
   "date": "2026-09-24T23:37:33Z",
   "message": "contrib: exclude and report expired signatures\n\nExclude expired keys from the quorum regardless of when they signed.\nReturn their signatures separately on quorum failure and in successful JSON output from both commands.\n\nCo-authored-by: Rob Hamilton <6456095+Rob1Ham@users.noreply.github.com>"
  },
  {
   "sha": "eff64984f5994347f8e28aa57e0394f80c87129b",
   "date": "2026-09-24T23:37:33Z",
   "message": "contrib: allow expired keys for old releases\n\nOld releases may have signatures from keys that expired later.\nAllow these to count with `--allow-expired` or `BINVERIFY_ALLOW_EXPIRED`, while continuing to exclude revoked keys and report expired signatures separately."
  },
  {
   "sha": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "date": "2026-09-24T23:37:33Z",
   "message": "doc: explain inactive binary signature handling"
  }
 ],
 "timeline": [
  {
   "t": "2026-09-18T07:06:39Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "i get revoked, but are we sure we want to exclude expired keys too?\n\nwill this break verifying old releases or current releases in the future?\n\ni\u2019ve noticed GPG keys lapse (in general, not only in this project) reasonably frequently and this doesn\u2019t mean they\u2019re compromised or bad.",
   "id": "c:5726467814",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5726467814",
   "hash": "a10189dc7141ee65",
   "edited": null
  },
  {
   "t": "2026-09-18T07:59:20Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI think that is the correct thing to do for revoked keys. What to do with expired keys has been a debate since ~forever. How about categorizing sigs from expired keys separately and adding a flag that allows the user to treat them as good?",
   "id": "c:5727040666",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5727040666",
   "hash": "694c407e63f07af2",
   "edited": null
  },
  {
   "t": "2026-09-18T08:10:19Z",
   "kind": "comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI could see that as reasonable. With that optionality a user of \"current software, today\" should ideally see majority non-expired keys verified by default. Somebody targeting older software (perhaps with automated build tools) could set the optional flag to include expired keys.\n\nWe should prioritise \"verifying current software today\" in the most robust way possible. That's the less interesting/important use-case vs \"historical verifications\" where keys may have expired...",
   "id": "c:5727162821",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5727162821",
   "hash": "c8538d59696a27e1",
   "edited": null
  },
  {
   "t": "2026-09-18T21:46:03Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "b956ee67ffbd2700b8f2be0bf63a2bdc7f31170a",
   "id": "push:b956ee67ffbd2700b8f2be0bf63a2bdc7f31170a"
  },
  {
   "t": "2026-09-18T21:51:43Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Added `--allow-expired` for historical verification, with expired signatures reported separately and revoked signatures always excluded - also simplified the tests and reorganized the commits, thanks for the quick reviews.",
   "id": "c:5736621483",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5736621483",
   "hash": "3edfcbef627d7ef8",
   "edited": null
  },
  {
   "t": "2026-09-23T10:12:52Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "b956ee67ffbd2700b8f2be0bf63a2bdc7f31170a",
   "text": "The end result looks decent, but the development sequence seems to change the same lines as the behavior changes through its flow.\n\nNot a blocker for me, especially given the scope of this code, but I wonder whether a more \"squashed\" sequence, such as [this](https://github.com/willcl-ark/bitcoin/tree/pr-36293), would reduce the churn (and make the history easier to follow and bisect if ever needed)?\n\nIf you prefer your more \"natural development\" sequence, I can probably be persuaded it's fine too, though.",
   "id": "r:5289620888",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#pullrequestreview-5289620888",
   "hash": "e6d40b451879e1ef",
   "edited": null
  },
  {
   "t": "2026-09-23T18:01:10Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYour version is also a good way to solve it, but it has bigger conceptual jumps. I deliberately structured it so that I cover the behavior first before modifying it, adding the safety net before the jump. We get to the solution in trivial steps. It's like solving an equation while only doing one simplification at a time, to guarantee we're not skipping any steps.\nI document what the current behavior is to be able to prove that the fix changes that behavior, and to show exactly what the extent of the change is (keeping the test setup the same and only adjusting the assertions, so the diff shows both the before and after states and proving that both are passing CI).",
   "id": "c:5800159879",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5800159879",
   "hash": "6a220746fa774d42",
   "edited": null
  },
  {
   "t": "2026-09-24T00:40:14Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "73534a2407bbb8d72af33ebad7cb6d59a348af23",
   "id": "push:73534a2407bbb8d72af33ebad7cb6d59a348af23"
  },
  {
   "t": "2026-09-24T00:42:11Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks @willcl-ark for prompting another look.\nI combined your version and kept my structure expressed above (and inverted the revoked/expired commit order, which reduced a lot of TODO churn): I like this version more, let me know what you think.\nThe review also uncovered a case where a key can be both revoked and expired: revocation now takes precedence.",
   "id": "c:5805376940",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#issuecomment-5805376940",
   "hash": "f305a3e226a85683",
   "edited": "2026-09-24T00:42:30Z"
  },
  {
   "t": "2026-09-24T07:43:41Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "contrib/verify-binaries/verify.py",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "in_reply_to": null,
   "text": "In a984c3efc659296670df86c4b99e06d913c77f4e\n\nDo you think the test should have an assertion for these `expired_sigs` fields?",
   "id": "rc:4091125514",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#discussion_r4091125514",
   "hash": "ff042420959ac152",
   "edited": "2026-09-24T07:52:54Z"
  },
  {
   "t": "2026-09-24T07:48:09Z",
   "kind": "review_comment",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "path": "contrib/verify-binaries/verify.py",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "in_reply_to": null,
   "text": "In a984c3efc659296670df86c4b99e06d913c77f4e\n\nNot introduced in this PR, but in reading the changes above I notice we just sum the lengths here for a count. I haven't tested in GPG, but I'm not wondering whether I could provide 10 signatures from one key, and have this verify successfully; should we be de-duplicating by key or fingerprint perhaps?\n\nAnyway, probably for a followup...",
   "id": "rc:4091158778",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#discussion_r4091158778",
   "hash": "511eee6696d54f9f",
   "edited": "2026-09-24T07:52:54Z"
  },
  {
   "t": "2026-09-24T07:52:54Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "73534a2407bbb8d72af33ebad7cb6d59a348af23",
   "text": "As I said, I can be persuaded that a totally squashed commit series is not necessary here. I feel OK with this current one, thanks for updating.\n\nOne other side-thought I had thinking about this PR is that we are relying on local gpg (revocation) data/statuses being up-to-date for this to work. (Well, for revoked keys at least, which  are perhaps the most important).\n\nI wonder if we should prompt/remind users that they should ideally update keys periodically. I don't think we want to initiate that ourselves, as fetching keys is not a private process unless done carefully, so we might otherwise \"dox our users\" as bitcoiners...",
   "id": "r:5301316723",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#pullrequestreview-5301316723",
   "hash": "49bfc5ec59b7a8b8",
   "edited": null
  },
  {
   "t": "2026-09-25T01:01:37Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "id": "push:034271d4e1739347a31c64dbc4bfef8aa133ad1d"
  },
  {
   "t": "2026-09-25T01:03:42Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "contrib/verify-binaries/verify.py",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "in_reply_to": 4091125514,
   "text": "Sure, the `pub --json` test now checks that `expired_sigs` is a list, and the quorum test checks the expired signature\u2019s full representation.",
   "id": "rc:4100026521",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#discussion_r4100026521",
   "hash": "fecdd6b45d31dbc9",
   "edited": "2026-09-25T01:11:55Z"
  },
  {
   "t": "2026-09-25T01:05:21Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "contrib/verify-binaries/verify.py",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "in_reply_to": 4091158778,
   "text": "I also got suspicious of this one and agree, we should address this in a follow-up.\nI confirmed with a disposable GPG key that repeating one signature can satisfy a threshold of two.\nFor the default path, `len({sig.key for sig in good_trusted + good_untrusted})` would catch that case, though counting distinct primary keys probably needs more care.",
   "id": "rc:4100037715",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#discussion_r4100037715",
   "hash": "8e0d1ddc44d390a4",
   "edited": "2026-09-25T01:11:55Z"
  },
  {
   "t": "2026-09-25T01:11:55Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "text": "[quoted text omitted]\n\nThe latest push adds a short README note that stale local keyrings may miss revocations and keyserver updates can reveal requested keys - in a non-patronizing way.\nIt also checks `expired_sigs` in the JSON test, covers both orders of the expired and revoked GPG statuses.",
   "id": "r:5312073949",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#pullrequestreview-5312073949",
   "hash": "6e3f65ca59ffe253",
   "edited": null
  },
  {
   "t": "2026-09-25T09:31:25Z",
   "kind": "review",
   "who": "willcl-ark",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
   "text": "ACK 034271d4e1739347a31c64dbc4bfef8aa133ad1d\n\nI verified recent releases with and without `--allow-expired`:\n\n| Release | Counted by default | Expired | Counted with `--allow-expired` | Result |\n| --- | ---: | ---: | ---: | --- |\n| 22.1 | 9 | 5 | 14 | Pass |\n| 23.2 | 7 | 5 | 12 | Pass |\n| 24.2 | 8 | 4 | 12 | Pass |\n| 25.2 | 5 | 2 | 7 | Pass |\n| 26.2 | 6 | 4 | 10 | Pass |\n| 27.2 | 8 | 3 | 11 | Pass |\n| 28.4 | 8 | 2 | 10 | Pass |\n| 29.4 | 9 | 3 | 12 | Pass |\n| 30.3 | 8 | 2 | 10 | Pass |\n| 31.1 | 8 | 2 | 10 | Pass |\n\nAll releases passed the default (three-signature) quorum with my local keyring. None raised an exception.\n\nAll seems well.\n\nI think distinct keys could be an interesting belt-and-braces followup still, if it's reasonable/tractable.",
   "id": "r:5315970979",
   "url": "https://github.com/bitcoin/bitcoin/pull/36293#pullrequestreview-5315970979",
   "hash": "f7fbc46752bbdfdf",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-09-17T23:21:12Z",
   "action": "labeled",
   "label": "Scripts and tools",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-09-18T18:10:40Z",
   "kind": "renamed",
   "who": "l0rinc",
   "from": "contrib: prevent inactive keys from satisfying the binary verification quorum",
   "to": "contrib: exclude inactive keys from the binary quorum by default"
  }
 ],
 "text_chars": 7650,
 "text_tokens_estimate": 1912,
 "changed_paths": [
  "contrib/verify-binaries/README.md",
  "contrib/verify-binaries/test.py",
  "contrib/verify-binaries/verify.py"
 ],
 "files": [
  {
   "path": "contrib/verify-binaries/README.md",
   "add": 15,
   "del": 1
  },
  {
   "path": "contrib/verify-binaries/test.py",
   "add": 72,
   "del": 0
  },
  {
   "path": "contrib/verify-binaries/verify.py",
   "add": 38,
   "del": 14
  }
 ],
 "test_lines": 140,
 "git": {
  "head": "034271d4e1739347a31c64dbc4bfef8aa133ad1d",
  "head_matches_backup": true,
  "base": "d48e76e689bb4d680d9d8a70b67f994be2f0cdd1",
  "patch_id": "1d1e041c2be4e088",
  "commits": [
   {
    "sha": "431cac7273",
    "subject": "test: characterize inactive signature quorum",
    "files": 2,
    "add": 87,
    "del": 10
   },
   {
    "sha": "20e15a5fdc",
    "subject": "contrib: exclude revoked keys from binary quorum",
    "files": 2,
    "add": 14,
    "del": 7
   },
   {
    "sha": "02939e424d",
    "subject": "contrib: exclude and report expired signatures",
    "files": 2,
    "add": 16,
    "del": 11
   },
   {
    "sha": "eff64984f5",
    "subject": "contrib: allow expired keys for old releases",
    "files": 2,
    "add": 10,
    "del": 3
   },
   {
    "sha": "034271d4e1",
    "subject": "doc: explain inactive binary signature handling",
    "files": 1,
    "add": 15,
    "del": 1
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "1d1e041c2be4e088",
 "input_hash": "c0ea848b24e5e521",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}