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.
1#!/usr/bin/env python32# Copyright (c) 2018-present The Bitcoin Core developers3# Distributed under the MIT software license, see the accompanying4# file COPYING or http://www.opensource.org/licenses/mit-license.php.5"""Verify commits against a trusted keys list."""6import argparse7import hashlib8import logging9import os10from pathlib import Path11import subprocess12import sys13import time15GIT = os.getenv('GIT', 'git')17def is_ancestor(older, newer, root_name):18"""Return whether older is an ancestor of newer, rejecting Git errors."""19result = subprocess.run([GIT, "merge-base", "--is-ancestor", older, newer])20if result.returncode not in (0, 1):21print(f'Failed to determine ancestry between "{older}" and "{newer}" for the {root_name} (git merge-base exited with {result.returncode}).', file=sys.stderr)22sys.exit(1)23return result.returncode == 025def predates(commit, root, root_name):26"""Return whether commit is provably older than root, rejecting divergent history."""27if is_ancestor(root, commit, root_name):28return False29elif is_ancestor(commit, root, root_name):30return True31print(f'"{commit}" diverges from the {root_name} "{root}", refusing to verify.', file=sys.stderr)32sys.exit(1)34def tree_sha512sum(commit='HEAD'):35"""Calculate the Tree-sha512 for the commit.37This is copied from github-merge.py. See https://github.com/bitcoin-core/bitcoin-maintainer-tools."""39# request metadata for entire tree, recursively40files = []41blob_by_name = {}42for line in subprocess.check_output([GIT, 'ls-tree', '--full-tree', '-r', commit]).splitlines():43name_sep = line.index(b'\t')44metadata = line[:name_sep].split() # perms, 'blob', blobid45assert metadata[1] == b'blob'46name = line[name_sep + 1:]47files.append(name)48blob_by_name[name] = metadata[2]50files.sort()51# open connection to git-cat-file in batch mode to request data for all blobs52# this is much faster than launching it per file53p = subprocess.Popen([GIT, 'cat-file', '--batch'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)54overall = hashlib.sha512()55for f in files:56blob = blob_by_name[f]57# request blob58p.stdin.write(blob + b'\n')59p.stdin.flush()60# read header: blob, "blob", size61reply = p.stdout.readline().split()62assert reply[0] == blob and reply[1] == b'blob'63size = int(reply[2])64# hash the blob data65intern = hashlib.sha512()66ptr = 067while ptr < size:68bs = min(65536, size - ptr)69piece = p.stdout.read(bs)70if len(piece) == bs:71intern.update(piece)72else:73raise IOError('Premature EOF reading git cat-file output')74ptr += bs75dig = intern.hexdigest()76assert p.stdout.read(1) == b'\n' # ignore LF that follows blob data77# update overall hash with file hash78overall.update(dig.encode("utf-8"))79overall.update(" ".encode("utf-8"))80overall.update(f)81overall.update("\n".encode("utf-8"))82p.stdin.close()83if p.wait():84raise IOError('Non-zero return value executing git cat-file')85return overall.hexdigest()87def main():89# Enable debug logging if running in CI90if 'CI' in os.environ and os.environ['CI'].lower() == "true":91logging.getLogger().setLevel(logging.DEBUG)93# Parse arguments94parser = argparse.ArgumentParser(usage='%(prog)s [options] [commit id]')95parser.add_argument('--disable-tree-check', action='store_false', dest='verify_tree', help='disable SHA-512 tree check')96parser.add_argument('--clean-merge', type=float, dest='clean_merge', default=float('inf'), help='Only check clean merge after <NUMBER> days ago (default: %(default)s)', metavar='NUMBER')97parser.add_argument('commit', nargs='?', default='HEAD', help='Check clean merge up to commit <commit>')98args = parser.parse_args()100# get directory of this program and read data files101dirname = Path(__file__).absolute().parent102print(f"Using verify-commits data from {dirname}")103verified_root = (dirname / "trusted-git-root").read_text().splitlines()[0]104verified_sha512_root = (dirname / "trusted-sha512-root-commit").read_text().splitlines()[0]105revsig_allowed = (dirname / "allow-revsig-commits").read_text().splitlines()106unclean_merge_allowed = (dirname / "allow-unclean-merge-commits").read_text().splitlines()107incorrect_sha512_allowed = (dirname / "allow-incorrect-sha512-commits").read_text().splitlines()108trusted_keys = (dirname / "trusted-keys").read_text().splitlines()110# Set commit and variables111current_commit = args.commit112if ' ' in current_commit:113print("Commit must not contain spaces", file=sys.stderr)114sys.exit(1)115verify_tree = args.verify_tree116no_sha1 = True117prev_commit = ""118initial_commit = current_commit120# Iterate through commits121while True:123# Log a message to prevent Travis from timing out124logging.debug("verify-commits: [in-progress] processing commit {}".format(current_commit[:8]))126if current_commit == verified_root:127# Ensure the trusted root identifies an existing commit.128is_ancestor(verified_root, current_commit, "trusted Git root")129print('There is a valid path from "{}" to {} where all commits are signed!'.format(initial_commit, verified_root))130sys.exit(0)131elif predates(current_commit, verified_root, "trusted Git root"):132print(f"\"{current_commit}\" predates the trusted root, stopping!")133sys.exit(0)135if verify_tree:136if current_commit == verified_sha512_root:137print("All Tree-SHA512s matched up to {}".format(verified_sha512_root), file=sys.stderr)138verify_tree = False139no_sha1 = False140elif predates(current_commit, verified_sha512_root, "trusted Tree-SHA512 root"):141print(f"\"{current_commit}\" predates the trusted SHA512 root, disabling tree verification.")142verify_tree = False143no_sha1 = False146os.environ['BITCOIN_VERIFY_COMMITS_ALLOW_SHA1'] = "0" if no_sha1 else "1"147allow_revsig = current_commit in revsig_allowed149# Check that the commit (and parents) was signed with a trusted key150valid_sig = False151verify_res = subprocess.run([GIT, '-c', 'gpg.program={}/gpg.sh'.format(dirname), 'verify-commit', "--raw", current_commit], text=True, capture_output=True)152for line in verify_res.stderr.splitlines():153if line.startswith("[GNUPG:] VALIDSIG "):154key = line.split(" ")[-1]155valid_sig = key in trusted_keys156elif (line.startswith("[GNUPG:] REVKEYSIG ") or line.startswith("[GNUPG:] EXPKEYSIG ")) and not allow_revsig:157valid_sig = False158break159if not valid_sig:160if prev_commit != "":161print("No parent of {} was signed with a trusted key!".format(prev_commit), file=sys.stderr)162print("Parents are:", file=sys.stderr)163parents = subprocess.check_output([GIT, 'show', '-s', '--format=format:%P', prev_commit], text=True).splitlines()[0].split(' ')164for parent in parents:165subprocess.call([GIT, 'show', '-s', parent], stdout=sys.stderr)166else:167print("{} was not signed with a trusted key!".format(current_commit), file=sys.stderr)168sys.exit(1)170# Check the Tree-SHA512171if (verify_tree or prev_commit == "") and current_commit not in incorrect_sha512_allowed:172tree_hash = tree_sha512sum(current_commit)173if ("Tree-SHA512: {}".format(tree_hash)) not in subprocess.check_output([GIT, 'show', '-s', '--format=format:%B', current_commit], text=True).splitlines():174print("Tree-SHA512 did not match for commit " + current_commit, file=sys.stderr)175sys.exit(1)177# Merge commits should only have two parents178parents = subprocess.check_output([GIT, 'show', '-s', '--format=format:%P', current_commit], text=True).splitlines()[0].split(' ')179if len(parents) > 2:180print("Commit {} is an octopus merge".format(current_commit), file=sys.stderr)181sys.exit(1)183# Check that the merge commit is clean184commit_time = int(subprocess.check_output([GIT, 'show', '-s', '--format=format:%ct', current_commit], text=True).splitlines()[0])185check_merge = commit_time > time.time() - args.clean_merge * 24 * 60 * 60 # Only check commits in clean_merge days186allow_unclean = current_commit in unclean_merge_allowed187if len(parents) == 2 and check_merge and not allow_unclean:188current_tree = subprocess.check_output([GIT, 'show', '--format=%T', current_commit], text=True).splitlines()[0]190# This merge-tree functionality requires git >= 2.38. The191# --write-tree option was added in order to opt-in to the new192# behavior. Older versions of git will not recognize the option and193# will instead exit with code 128.194try:195recreated_tree = subprocess.check_output([GIT, "merge-tree", "--write-tree", parents[0], parents[1]], text=True).splitlines()[0]196except subprocess.CalledProcessError as e:197if e.returncode == 128:198print("git v2.38+ is required for this functionality.", file=sys.stderr)199sys.exit(1)200else:201raise e203if current_tree != recreated_tree:204print("Merge commit {} is not clean".format(current_commit), file=sys.stderr)205subprocess.call([GIT, 'diff', recreated_tree, current_tree])206sys.exit(1)208prev_commit = current_commit209current_commit = parents[0]211if __name__ == '__main__':212main()
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.