Repository: bitcoin/bitcoin (language: cpp)

Issue:
bug: verify-binaries/verify.py incorrectly parses version string; gives error or downloads wrong files
### Current behaviour

When I run:
`./verify.py pub 27.0-x86_64-linux-gnu` instead of downloading the version I told it, it downloads every version in the SHA256SUMS file starting with the first.

The `version_os` and (also `version_rc`) are completely ignored which causes wrong files to download when an -rcN is intended.

When I attempt to work around and give it a 1 dash string such as
`./verify.py pub 27.0-x86_64` it will start with bitcoin-27.0-x86_64-apple-darwin which is not useful if I only needed bitcoin-27.0-x86_64-linux-gnu.

Update: it will also parse `"27.0-aarch64"` wrong. Setting `rc_version="aarch64"` which causes no file found errors. But this is a valid string.

### Expected behaviour

I expected it to only download bitcoin-27.0-x86_64-linux-gnu.tar.gz when I run `./verify.py pub 27.0-x86_64-linux-gnu`.
I expected `rc_version` to be set to the `-rcN` release candidate number as per the comments and documentation.

### Steps to reproduce

Follow the instructions in the README.md 
> If you only want to download the binaries for a certain platform, add the corresponding suffix, e.g.:

But instead of the examples, give it a platform suffix with any dashes and it will ignore the requested platform.
Or give "<version>-aarch64" and aarch64 it will be interpreted as a release candidate number causing errors.

### Relevant log output

```
./verify.py pub 27.0-x86_64-linux-gnu
[INFO] got file https://bitcoincore.org/bin/bitcoin-core-27.0/SHA256SUMS.asc as SHA256SUMS.asc
[WARNING] https://bitcoin.org failed to provide file (https://bitcoin.org/bin/bitcoin-core-27.0/SHA256SUMS.asc). Continuing based solely upon https://bitcoincore.org.
[INFO] got file https://bitcoincore.org/bin/bitcoin-core-27.0/SHA256SUMS as SHA256SUMS
[WARNING] https://bitcoin.org failed to provide file (https://bitcoin.org/bin/bitcoin-core-27.0/SHA256SUMS). Continuing based solely upon https://bitcoincore.org.
[INFO] got 3 good signatures
[INFO] GOOD SIGNATURE (untrusted): SigData('1E4AED62986CD25D', 'Wladimir J. van der Laan <laanwj@protonmail.com>', trusted=False, status='')
[INFO] GOOD SIGNATURE (untrusted): SigData('17565732E08E5E41', 'Ava Chow <me@achow101.com>', trusted=False, status='')
[INFO] GOOD SIGNATURE (untrusted): SigData('3152347D07DA627C', 'Stephan Oeste (it) <it@oeste.de>', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('410108112E7EA81F', 'hebasto@gmail.com', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('C2371D91CB716EA7', 'sebastian.falbesoner@gmail.com', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('E7E2984B6289C93A', 'pinheadmz@gmail.com', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('2EEB9F5CC09526C1', 'fanquake@gmail.com', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('F62711DBDCA8AE56', 'kvaciral@protonmail.com', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('57FF9BDBCC301009', '', trusted=False, status='')
[WARNING] UNKNOWN SIGNATURE: SigData('8E4256593F177720', 'gugger@gmail.com', trusted=False, status='')
[INFO] removing *-unsigned binaries (bitcoin-27.0-arm64-apple-darwin-unsigned.tar.gz, bitcoin-27.0-arm64-apple-darwin-unsigned.zip, bitcoin-27.0-x86_64-apple-darwin-unsigned.tar.gz, bitcoin-27.0-x86_64-apple-darwin-unsigned.zip, bitcoin-27.0-win64-setup-unsigned.exe, bitcoin-27.0-win64-unsigned.tar.gz) from verification since https://bitcoincore.org does not host *-unsigned binaries
[INFO] removing *-debug binaries (bitcoin-27.0-aarch64-linux-gnu-debug.tar.gz, bitcoin-27.0-arm-linux-gnueabihf-debug.tar.gz, bitcoin-27.0-powerpc64-linux-gnu-debug.tar.gz, bitcoin-27.0-powerpc64le-linux-gnu-debug.tar.gz, bitcoin-27.0-riscv64-linux-gnu-debug.tar.gz, bitcoin-27.0-x86_64-linux-gnu-debug.tar.gz, bitcoin-27.0-win64-debug.zip) from verification since https://bitcoincore.org does not host *-debug binaries
[INFO] removing *-codesignatures binaries (bitcoin-27.0-codesignatures-27.0.tar.gz) from verification since https://bitcoincore.org does not host *-codesignatures binaries
[INFO] downloading bitcoin-27.0-aarch64-linux-gnu.tar.gz to /tmp/bitcoin_verify_binaries.27.0-x86_64-linux-gnu
```

```
amnesia@amnesia:~/.local/share/bitcoin-core$ ./verify.py pub 27.0-aarch64
[ERROR] couldn't fetch file (https://bitcoincore.org/bin/bitcoin-core-27.0/test.aarch64/SHA256SUMS.asc). Have you specified the version number in the following format?
<major>.<minor>[.<patch>][-rc[0-9]][-platform] (example: 22.0-x86_64 or 23.1-rc1-darwin)
wget output:
  --2024-05-21 12:28:08--  https://bitcoincore.org/bin/bitcoin-core-27.0/test.aarch64/SHA256SUMS.asc
  Resolving bitcoincore.org (bitcoincore.org)... 198.251.83.116
  Connecting to bitcoincore.org (bitcoincore.org)|198.251.83.116|:443... connected.
  HTTP request sent, awaiting response... 404 Not Found
  2024-05-21 12:28:14 ERROR 404: Not Found.
```

### How did you obtain Bitcoin Core

git clone

### What version of Bitcoin Core are you using?

master / 27.0

### Operating system and version

Debian 12

### Machine specifications

VM, 4GB RAM, virtIO SSD, wired


Discussion / hints:
I would like to be assigned to this issue and will send a PR in an hour.

Fix patch:
diff --git a/contrib/verify-binaries/README.md b/contrib/verify-binaries/README.md
index 04d683e69b559..0f3e16a5bc9d3 100644
--- a/contrib/verify-binaries/README.md
+++ b/contrib/verify-binaries/README.md
@@ -50,6 +50,7 @@ Get JSON output and don't prompt for user input (no auto key import):
 
 ```sh
 ./contrib/verify-binaries/verify.py --json pub 22.0-x86
+./contrib/verify-binaries/verify.py --json pub 23.0-rc5-linux-gnu
 ```
 
 Rely only on local GPG state and manually specified keys, while requiring a
@@ -57,14 +58,15 @@ threshold of at least 10 trusted signatures:
 ```sh
 ./contrib/verify-binaries/verify.py \
     --trusted-keys 74E2DEF5D77260B98BC19438099BAD163C70FBFA,9D3CC86A72F8494342EA5FD10A41BDC3F4FAFF1C \
-    --min-good-sigs 10 pub 22.0-x86
+    --min-good-sigs 10 pub 22.0-linux
 ```
 
-If you only want to download the binaries for a certain platform, add the corresponding suffix, e.g.:
+If you only want to download the binaries for a certain architecture and/or platform, add the corresponding suffix, e.g.:
 
 ```sh
-./contrib/verify-binaries/verify.py pub 24.0.1-darwin
-./contrib/verify-binaries/verify.py pub 23.1-rc1-win64
+./contrib/verify-binaries/verify.py pub 25.2-x86_64-linux
+./contrib/verify-binaries/verify.py pub 24.1-rc1-darwin
+./contrib/verify-binaries/verify.py pub 27.0-win64-setup.exe
 ```
 
 If you do not want to keep the downloaded binaries, specify the cleanup option.
diff --git a/contrib/verify-binaries/verify.py b/contrib/verify-binaries/verify.py
index 12e6e10d8a4de..6c07b36c9d8cd 100755
--- a/contrib/verify-binaries/verify.py
+++ b/contrib/verify-binaries/verify.py
@@ -97,23 +97,17 @@ def bool_from_env(key, default=False) -> bool:
 
 
 VERSION_FORMAT = "<major>.<minor>[.<patch>][-rc[0-9]][-platform]"
-VERSION_EXAMPLE = "22.0-x86_64 or 23.1-rc1-darwin"
+VERSION_EXAMPLE = "22.0 or 23.1-rc1-darwin.dmg or 27.0-x86_64-linux-gnu"
 
 def parse_version_string(version_str):
-    parts = version_str.split('-')
-    version_base = parts[0]
-    version_rc = ""
-    version_os = ""
-    if len(parts) == 2:  # "<version>-rcN" or "version-platform"
-        if "rc" in parts[1]:
-            version_rc = parts[1]
-        else:
-            version_os = parts[1]
-    elif len(parts) == 3:  # "<version>-rcN-platform"
-        version_rc = parts[1]
-        version_os = parts[2]
+    # "<version>[-rcN][-platform]"
+    version_base, _, platform = version_str.partition('-')
+    rc = ""
+    if platform.startswith("rc"): # "<version>-rcN[-platform]"
+        rc, _, platform = platform.partition('-')
+    # else "<version>" or "<version>-platform"
 
-    return version_base, version_rc, version_os
+    return version_base, rc, platform
 
 
 def download_with_wget(remote_file, local_file):
@@ -514,7 +508,9 @@ def cleanup():
     # Extract hashes and filenames
     hashes_to_verify = parse_sums_file(SUMS_FILENAME, [os_filter])
     if not hashes_to_verify:
-        log.error("no files matched the platform specified")
+        available_versions = ["-".join(line[1].split("-")[2:]) for line in parse_sums_file(SUMS_FILENAME, [])]
+        closest_match = difflib.get_close_matches(os_filter, available_versions, cutoff=0, n=1)[0]
+        log.error(f"No files matched the platform specified. Did you mean: {closest_match}")
         return ReturnCode.NO_BINARIES_MATCH
 
     # remove binaries that are known not to be hosted by bitcoincore.org


Test patch:
diff --git a/contrib/verify-binaries/test.py b/contrib/verify-binaries/test.py
index 22d718ece334a..875606ec22678 100755
--- a/contrib/verify-binaries/test.py
+++ b/contrib/verify-binaries/test.py
@@ -12,6 +12,21 @@ def main():
     expect_code(run_verify("", "pub", '0.32.awefa.12f9h'), 11, "Malformed version should fail")
     expect_code(run_verify('--min-good-sigs 20', "pub", "22.0"), 9, "--min-good-sigs 20 should fail")
 
+    print("- testing verification (22.0-x86_64-linux-gnu.tar.gz)", flush=True)
+    _220_x86_64_linux_gnu = run_verify("--json", "pub", "22.0-x86_64-linux-gnu.tar.gz")
+    try:
+        result = json.loads(_220_x86_64_linux_gnu.stdout.decode())
+    except Exception:
+        print("failed on 22.0-x86_64-linux-gnu.tar.gz --json:")
+        print_process_failure(_220_x86_64_linux_gnu)
+        raise
+
+    expect_code(_220_x86_64_linux_gnu, 0, "22.0-x86_64-linux-gnu.tar.gz should succeed")
+    v = result['verified_binaries']
+    assert result['good_trusted_sigs']
+    assert len(v) == 1
+    assert v['bitcoin-22.0-x86_64-linux-gnu.tar.gz'] == '59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16'
+
     print("- testing verification (22.0)", flush=True)
     _220 = run_verify("--json", "pub", "22.0")
     try: