{"uuid": "2121c430-6f81-4733-96e1-71560805413f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2012-2459", "type": "seen", "source": "https://gist.github.com/staccDOTsol/25cf0b07397868566d8d19a9ce0433d8", "content": "\"\"\"Pure-Python SPV primitives: headers, proof-of-work, partial merkle trees.\n\nAll hashes are handled in Bitcoin *internal* byte order (the natural byte\norder of dsha256 digests, i.e. little-endian uint256 display convention);\ndisplay hex (= reversed) is only produced at API boundaries.\n\"\"\"\nimport hashlib\nimport struct\n\nHEADER_LEN = 80\n\n\ndef dsha256(b):\n    return hashlib.sha256(hashlib.sha256(b).digest()).digest()\n\n\n# ---------------------------------------------------------------- block header\n\ndef parse_header(b):\n    if len(b) != HEADER_LEN:\n        raise ValueError(f\"header must be {HEADER_LEN} bytes, got {len(b)}\")\n    version, = struct.unpack(\" 256-bit target integer.\"\"\"\n    size = nbits &gt;&gt; 24\n    word = nbits &amp; 0x007FFFFF\n    if nbits &amp; 0x00800000:\n        raise ValueError(\"negative target in nBits\")\n    if word == 0:\n        raise ValueError(\"zero target in nBits\")\n    if size &lt;= 3:\n        return word &gt;&gt; (8 * (3 - size))\n    return word &lt;&lt; (8 * (size - 3))\n\n\ndef check_pow(h):\n    \"\"\"Raise unless uint256(dsha256(header)) &lt;= target(nBits).\"\"\"\n    target = bits_to_target(h[\"bits\"])\n    if target &gt;= 1 &lt;&lt; 256:\n        raise ValueError(\"target out of range\")\n    val = int.from_bytes(header_hash(h), \"little\")\n    if val &gt; target:\n        raise ValueError(\n            f\"insufficient proof of work: hash {val:064x} &gt; target {target:064x}\")\n\n\ndef verify_header_link(child, parent):\n    \"\"\"Raise unless child.prevhash == dsha256(parent header).\"\"\"\n    if child[\"prevhash\"] != header_hash(parent):\n        raise ValueError(\"header does not link to its parent (prevhash mismatch)\")\n\n\n# ------------------------------------------------------- partial merkle trees\n#\n# Serialized CMerkleBlock (as returned by `gettxoutproof`):\n#   80B header | uint32 nTx | varint nHashes | nHashes*32B |\n#   varint nFlagBytes | flag bytes\n#\n# Verification re-runs Bitcoin Core's recursive traversal\n# (CPartialMerkleTree::ExtractMatches):\n#   * tree width at height h is (nTx + (1&lt;&gt; h\n#   * flag bits are consumed LSB-first within each byte\n#   * a node consumes one hash iff height==0 or flag==0\n#   * a leaf with flag==1 is a matched txid\n#   * internal nodes hash dsha256(left||right); when the right child is\n#     outside the tree width, the left child is duplicated\n#   * left == right at an internal node is rejected (CVE-2012-2459)\n\nclass _Reader:\n    def __init__(self, b):\n        self.b = b\n        self.i = 0\n\n    def read(self, n):\n        r = self.b[self.i:self.i + n]\n        if len(r) != n:\n            raise ValueError(\"truncated merkle block\")\n        self.i += n\n        return r\n\n    def varint(self):\n        ch = self.read(1)[0]\n        if ch &lt; 0xFD:\n            return ch\n        if ch == 0xFD:\n            return struct.unpack(\"&gt; h\n\n    height = 0\n    while width(height) &gt; 1:\n        height += 1\n\n    hash_idx = 0\n    bit_idx = 0\n    matches = []\n\n    def read_bit():\n        nonlocal bit_idx\n        if bit_idx &gt;= len(flags) * 8:\n            raise ValueError(\"ran out of flag bits\")\n        bit = (flags[bit_idx &gt;&gt; 3] &gt;&gt; (bit_idx &amp; 7)) &amp; 1\n        bit_idx += 1\n        return bit\n\n    def read_hash():\n        nonlocal hash_idx\n        if hash_idx &gt;= len(hashes):\n            raise ValueError(\"ran out of hashes\")\n        h = hashes[hash_idx]\n        hash_idx += 1\n        return h\n\n    def traverse(h, pos):\n        flag = read_bit()\n        if h == 0 or flag == 0:\n            node = read_hash()\n            if h == 0 and flag == 1:\n                matches.append(node)\n            return node\n        left = traverse(h - 1, pos * 2)\n        if pos * 2 + 1 &lt; width(h - 1):\n            right = traverse(h - 1, pos * 2 + 1)\n            if right == left:\n                raise ValueError(\"invalid merkle branch: duplicated hash \"\n                                 \"(CVE-2012-2459 mutation)\")\n        else:\n            right = left  # odd row: duplicate the left child\n        return dsha256(left + right)\n\n    root = traverse(height, 0)\n    if hash_idx != len(hashes):\n        raise ValueError(\"proof contains unused hashes\")\n    if (bit_idx + 7) // 8 != len(flags):\n        raise ValueError(\"proof contains unused flag bytes\")\n    return root, [m[::-1].hex() for m in matches]\n\n\ndef verify_merkle_proof(blob, expect_txid=None):\n    \"\"\"Full check of a gettxoutproof blob: PoW, merkle root, membership.\"\"\"\n    header, ntx, hashes, flags = parse_merkle_block(blob)\n    check_pow(header)\n    root, matches = extract_matches(ntx, hashes, flags)\n    if root != header[\"merkleroot\"]:\n        raise ValueError(\"computed merkle root != header merkleroot\")\n    if expect_txid is not None and expect_txid.lower() not in matches:\n        raise ValueError(f\"txid {expect_txid} not among matched leaves\")\n    return {\n        \"header\": header,\n        \"block_hash\": header_hash_hex(header),\n        \"merkle_root\": root,\n        \"matches\": matches,\n        \"ntx\": ntx,\n    }\n", "creation_timestamp": "2026-07-30T08:30:12.050115Z"}