Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
251"""252assert len(hosts) > 1253primary_host = hosts[0]254other_hosts = hosts[1:]255got_files = []257def join_url(host: str) -> str:258return host.rstrip('/') + '/' + path.lstrip('/')260url = join_url(primary_host)261success, output = download_with_wget(url, filename)262if not success:263log.error(264f"couldn't fetch file ({url}). "265"Have you specified the version number in the following format?\n"266f"{VERSION_FORMAT} "267f"(example: {VERSION_EXAMPLE})\n"268f"wget output:\n{indent(output)}")269return ReturnCode.FILE_GET_FAILED270else:271log.info(f"got file {url} as {filename}")272got_files.append(filename)274for i, host in enumerate(other_hosts):275url = join_url(host)276fname = filename + f'.{i + 2}'277success, output = download_with_wget(url, fname)279if require_all and not success:280log.error(281f"{host} failed to provide file ({url}), but {primary_host} did?\n"282f"wget output:\n{indent(output)}")283return ReturnCode.FILE_MISSING_FROM_ONE_HOST284elif not success:285log.warning(286f"{host} failed to provide file ({url}). "287f"Continuing based solely upon {primary_host}.")288else:289log.info(f"got file {url} as {fname}")290got_files.append(fname)292for i, got_file in enumerate(got_files):293if got_file == got_files[-1]:294break # break on last file, nothing after it to compare to296compare_to = got_files[i + 1]297if not files_are_equal(got_file, compare_to):298log.error(f"files not equal: {got_file} and {compare_to}")299return ReturnCode.FILES_NOT_EQUAL301return ReturnCode.SUCCESS304def check_multisig(sums_file: str, sigfilename: str, args: argparse.Namespace) -> tuple[int, str, list[SigData], list[SigData], list[SigData]]:305# check signature306#307# We don't write output to a file because this command will almost certainly308# fail with GPG exit code '2' (and so not writing to --output) because of the309# likely presence of multiple untrusted signatures.310retval, output = verify_with_gpg(sums_file, sigfilename)312if args.verbose:313log.info(f"gpg output:\n{indent(output)}")315good, unknown, bad = parse_gpg_result(output.splitlines())317if unknown and args.import_keys:318# Retrieve unknown keys and then try GPG again.319for unsig in unknown:320if prompt_yn(f" ? Retrieve key {unsig.key} ({unsig.name})? (y/N) "):321ran = subprocess.run(322["gpg", "--keyserver", args.keyserver, "--recv-keys", unsig.key])324if ran.returncode != 0:325log.warning(f"failed to retrieve key {unsig.key}")327# Reparse the GPG output now that we have more keys328retval, output = verify_with_gpg(sums_file, sigfilename)329good, unknown, bad = parse_gpg_result(output.splitlines())331return retval, output, good, unknown, bad334def prompt_yn(prompt) -> bool:335"""Return true if the user inputs 'y'."""336got = ''337while got not in ['y', 'n']:338got = input(prompt).lower()339return got == 'y'341def verify_shasums_signature(342signature_file_path: str, sums_file_path: str, args: argparse.Namespace343) -> tuple[344ReturnCode, list[SigData], list[SigData], list[SigData], list[SigData]345]:346min_good_sigs = args.min_good_sigs347gpg_allowed_codes = [0, 2] # 2 is returned when untrusted signatures are present.349gpg_retval, gpg_output, good, unknown, bad = check_multisig(sums_file_path, signature_file_path, args)351if gpg_retval not in gpg_allowed_codes:352if gpg_retval == 1:353log.critical(f"Bad signature (code: {gpg_retval}).")354else:355log.critical(f"unexpected GPG exit code ({gpg_retval})")357log.error(f"gpg output:\n{indent(gpg_output)}")358return (ReturnCode.INTEGRITY_FAILURE, [], [], [], [])360# Decide which keys we trust, though not "trust" in the GPG sense, but rather361# which pubkeys convince us that this sums file is legitimate. In other words,362# which pubkeys within the Bitcoin community do we trust for the purposes of363# binary verification?364trusted_keys = set()365if args.trusted_keys:366trusted_keys |= set(args.trusted_keys.split(','))368# Tally signatures and make sure we have enough goods to fulfill369# our threshold.370good_trusted = [sig for sig in good if sig.trusted or sig.key in trusted_keys]371good_untrusted = [sig for sig in good if sig not in good_trusted]372num_trusted = len(good_trusted) + len(good_untrusted)373log.info(f"got {num_trusted} good signatures")375if num_trusted < min_good_sigs:376log.info("Maybe you need to import "377f"(`gpg --keyserver {args.keyserver} --recv-keys <key-id>`) "378"some of the following keys: ")379log.info('')380for sig in unknown:381log.info(f" {sig.key} ({sig.name})")382log.info('')383log.error(384"not enough trusted sigs to meet threshold "385f"({num_trusted} vs. {min_good_sigs})")387return (ReturnCode.NOT_ENOUGH_GOOD_SIGS, [], [], [], [])389for sig in good_trusted:390log.info(f"GOOD SIGNATURE: {sig}")392for sig in good_untrusted:393log.info(f"GOOD SIGNATURE (untrusted): {sig}")395for sig in [sig for sig in good if sig.status == 'expired']:396log.warning(f"key {sig.key} for {sig.name} is expired")398for sig in bad:399log.warning(f"BAD SIGNATURE: {sig}")401for sig in unknown:402log.warning(f"UNKNOWN SIGNATURE: {sig}")404return (ReturnCode.SUCCESS, good_trusted, good_untrusted, unknown, bad)407def parse_sums_file(sums_file_path: str, filename_filter: list[str]) -> list[list[str]]:408# extract hashes/filenames of binaries to verify from hash file;409# each line has the following format: "<hash> <binary_filename>"410with open(sums_file_path, 'r') as hash_file:411return [line.split()[:2] for line in hash_file if len(filename_filter) == 0 or any(f in line for f in filename_filter)]414def verify_binary_hashes(hashes_to_verify: list[list[str]]) -> tuple[ReturnCode, dict[str, str]]:415offending_files = []416files_to_hashes = {}418for hash_expected, binary_filename in hashes_to_verify:419with open(binary_filename, 'rb') as binary_file:420hash_calculated = sha256(binary_file.read()).hexdigest()421if hash_calculated != hash_expected:422offending_files.append(binary_filename)423else:424files_to_hashes[binary_filename] = hash_calculated426if offending_files:427joined_files = '\n'.join(offending_files)428log.critical(429"Hashes don't match.\n"430f"Offending files:\n{joined_files}")431return (ReturnCode.INTEGRITY_FAILURE, files_to_hashes)433return (ReturnCode.SUCCESS, files_to_hashes)436def verify_published_handler(args: argparse.Namespace) -> ReturnCode:437WORKINGDIR = Path(tempfile.gettempdir()) / f"bitcoin_verify_binaries.{args.version}"439def cleanup():440log.info("cleaning up files")441os.chdir(Path.home())442shutil.rmtree(WORKINGDIR)444# determine remote dir dependent on provided version string445try:446version_base, version_rc, os_filter = parse_version_string(args.version)447version_tuple = [int(i) for i in version_base.split('.')]448except Exception as e:449log.debug(e)450log.error(f"unable to parse version; expected format is {VERSION_FORMAT}")451log.error(f" e.g. {VERSION_EXAMPLE}")452return ReturnCode.BAD_VERSION454remote_dir = f"/bin/{VERSIONPREFIX}{version_base}/"455if version_rc:456remote_dir += f"test.{version_rc}/"457remote_sigs_path = remote_dir + SIGNATUREFILENAME458remote_sums_path = remote_dir + SUMS_FILENAME460# create working directory461os.makedirs(WORKINGDIR, exist_ok=True)462os.chdir(WORKINGDIR)464hosts = [HOST1, HOST2]466got_sig_status = get_files_from_hosts_and_compare(467hosts, remote_sigs_path, SIGNATUREFILENAME, args.require_all_hosts)468if got_sig_status != ReturnCode.SUCCESS:469return got_sig_status471# Multi-sig verification is available after 22.0.472if version_tuple[0] < 22:473log.error("Version too old - single sig not supported. Use a previous "474"version of this script from the repo.")475return ReturnCode.BAD_VERSION477got_sums_status = get_files_from_hosts_and_compare(478hosts, remote_sums_path, SUMS_FILENAME, args.require_all_hosts)479if got_sums_status != ReturnCode.SUCCESS:480return got_sums_status482# Verify the signature on the SHA256SUMS file483sigs_status, good_trusted, good_untrusted, unknown, bad = verify_shasums_signature(SIGNATUREFILENAME, SUMS_FILENAME, args)484if sigs_status != ReturnCode.SUCCESS:485if sigs_status == ReturnCode.INTEGRITY_FAILURE:486cleanup()487return sigs_status489# Extract hashes and filenames490hashes_to_verify = parse_sums_file(SUMS_FILENAME, [os_filter])491if not hashes_to_verify:492available_versions = ["-".join(line[1].split("-")[2:]) for line in parse_sums_file(SUMS_FILENAME, [])]493closest_match = difflib.get_close_matches(os_filter, available_versions, cutoff=0, n=1)[0]494log.error(f"No files matched the platform specified. Did you mean: {closest_match}")495return ReturnCode.NO_BINARIES_MATCH497# remove binaries that are known not to be hosted by bitcoincore.org498fragments_to_remove = ['-unsigned', '-debug', '-codesignatures']499for fragment in fragments_to_remove:500nobinaries = [i for i in hashes_to_verify if fragment in i[1]]
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.