57 lines
2.4 KiB
Python
57 lines
2.4 KiB
Python
"""Merkle proofs (B-83, TX-22, ELC-26).
|
|||
|
|
|
||
|
|
`blockchain.transaction.get_merkle` answers with the branch that links a transaction to
|
||
|
|
its block's merkle root. Checking it against a header **we hold** is what turns "a server
|
||
|
|
says this tx is at height h" into something a hostile server cannot fake without mining.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import hashlib
|
||
|
|
|
||
|
|
_HEADER_LENGTH_BYTES = 80
|
||
|
|
_MERKLE_ROOT_OFFSET = 36
|
||
|
|
_MERKLE_ROOT_LENGTH = 32
|
||
|
|
_MAX_BRANCH_LENGTH = 40 # 2^40 transactions in a block is already absurd
|
||
|
|
|
||
|
|
|
||
|
|
def _sha256d(data: bytes) -> bytes:
|
||
|
|
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
|
||
|
|
|
||
|
|
|
||
|
|
def header_merkle_root(header_hex: str) -> bytes:
|
||
|
|
"""The merkle root committed to by a raw 80-byte header, in internal byte order."""
|
||
|
|
header = bytes.fromhex(header_hex)
|
||
|
|
if len(header) != _HEADER_LENGTH_BYTES:
|
||
|
|
raise ValueError(f"a block header is {_HEADER_LENGTH_BYTES} bytes, got {len(header)}")
|
||
|
|
return header[_MERKLE_ROOT_OFFSET : _MERKLE_ROOT_OFFSET + _MERKLE_ROOT_LENGTH]
|
||
|
|
|
||
|
|
|
||
|
|
def merkle_root_from_branch(txid: str, branch: list[str], position: int) -> bytes:
|
||
|
|
"""The merkle root (internal byte order) a transaction `txid` at index `position` of
|
||
|
|
its block reaches through `branch` — the sibling hashes, bottom-up, in the display
|
||
|
|
order Electrum uses for txids."""
|
||
|
|
if position < 0 or len(branch) > _MAX_BRANCH_LENGTH or position >> len(branch):
|
||
|
|
raise ValueError("merkle position does not fit the branch")
|
||
|
|
current = bytes.fromhex(txid)[::-1]
|
||
|
|
index = position
|
||
|
|
for sibling_hex in branch:
|
||
|
|
sibling = bytes.fromhex(sibling_hex)[::-1]
|
||
|
|
if len(sibling) != 32:
|
||
|
|
raise ValueError("merkle branch element is not 32 bytes")
|
||
|
|
current = _sha256d(sibling + current) if index & 1 else _sha256d(current + sibling)
|
||
|
|
index >>= 1
|
||
|
|
return current
|
||
|
|
|
||
|
|
|
||
|
|
def proof_matches_header(txid: str, height: int, proof: object, header_hex: str) -> bool:
|
||
|
|
"""Does `proof` — a `get_merkle` reply — put `txid` in the block at `height` whose
|
||
|
|
header is `header_hex`? Any malformed reply is simply "no"."""
|
||
|
|
try:
|
||
|
|
if not isinstance(proof, dict) or int(proof["block_height"]) != height:
|
||
|
|
return False
|
||
|
|
branch = proof["merkle"]
|
||
|
|
if not isinstance(branch, list):
|
||
|
|
return False
|
||
|
|
root = merkle_root_from_branch(txid, [str(item) for item in branch], int(proof["pos"]))
|
||
|
|
return root == header_merkle_root(header_hex)
|
||
|
|
except (KeyError, TypeError, ValueError):
|
||
|
|
return False
|