mirror of
https://github.com/bitcoin/bips.git
synced 2026-09-21 19:04:28 +00:00
Merge pull request #2273 from let-the-dreamers-rise/bip360-refimpl-depth-bound-and-raises
bip360: bound merkle depth at 128 and replace validation asserts in ref-impl
This commit is contained in:
@@ -23,6 +23,11 @@ BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|||||||
BECH32M_CONST = 0x2BC830A3
|
BECH32M_CONST = 0x2BC830A3
|
||||||
MAX_COMPACT_SIZE = 2**64 - 1
|
MAX_COMPACT_SIZE = 2**64 - 1
|
||||||
|
|
||||||
|
# BIP 360, Script Validation: a control block "must have length 1 + 32 * m, for a
|
||||||
|
# value of m that is an integer between 0 and 128, inclusive" -- so no script leaf
|
||||||
|
# may sit deeper than 128 branches, or its control block is consensus-invalid.
|
||||||
|
CONTROL_MAX_NODE_COUNT = 128
|
||||||
|
|
||||||
# A script tree node is either a leaf (dict) or a branch (list of nodes)
|
# A script tree node is either a leaf (dict) or a branch (list of nodes)
|
||||||
ScriptTree = Union[Dict[str, Any], List["ScriptTree"]]
|
ScriptTree = Union[Dict[str, Any], List["ScriptTree"]]
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ def tapbranch_hash(left: bytes, right: bytes) -> bytes:
|
|||||||
return tagged_hash("TapBranch", b"".join(sorted((left, right))))
|
return tagged_hash("TapBranch", b"".join(sorted((left, right))))
|
||||||
|
|
||||||
|
|
||||||
def compute_merkle_root(tree: ScriptTree) -> bytes:
|
def compute_merkle_root(tree: ScriptTree, depth: int = 0) -> bytes:
|
||||||
"""Recursively compute script tree merkle root"""
|
"""Recursively compute script tree merkle root"""
|
||||||
if isinstance(tree, dict): # Leaf
|
if isinstance(tree, dict): # Leaf
|
||||||
version = tree["leafVersion"]
|
version = tree["leafVersion"]
|
||||||
@@ -99,8 +104,15 @@ def compute_merkle_root(tree: ScriptTree) -> bytes:
|
|||||||
# Script trees are treated strictly as binary trees; each branch node should have
|
# Script trees are treated strictly as binary trees; each branch node should have
|
||||||
# exactly 2 children. This isn't a general n-ary fold, and combining
|
# exactly 2 children. This isn't a general n-ary fold, and combining
|
||||||
# more than 2 children sequentially would not produce a valid P2MR merkle root.
|
# more than 2 children sequentially would not produce a valid P2MR merkle root.
|
||||||
assert len(tree) == 2, f"expected binary branch, got {len(tree)} children"
|
if depth >= CONTROL_MAX_NODE_COUNT:
|
||||||
left, right = compute_merkle_root(tree[0]), compute_merkle_root(tree[1])
|
raise ValueError(
|
||||||
|
f"compute_merkle_root: leaf deeper than {CONTROL_MAX_NODE_COUNT}; "
|
||||||
|
"its control block would exceed the consensus length limit"
|
||||||
|
)
|
||||||
|
if len(tree) != 2:
|
||||||
|
raise ValueError(f"expected binary branch, got {len(tree)} children")
|
||||||
|
left = compute_merkle_root(tree[0], depth + 1)
|
||||||
|
right = compute_merkle_root(tree[1], depth + 1)
|
||||||
return tapbranch_hash(left, right)
|
return tapbranch_hash(left, right)
|
||||||
|
|
||||||
else: # badbadnotgood
|
else: # badbadnotgood
|
||||||
@@ -118,18 +130,26 @@ def compute_control_block(path: int, tree: ScriptTree) -> bytes:
|
|||||||
"""
|
"""
|
||||||
if isinstance(tree, dict):
|
if isinstance(tree, dict):
|
||||||
return bytes([tree["leafVersion"] | 1])
|
return bytes([tree["leafVersion"] | 1])
|
||||||
assert isinstance(tree, list) and len(tree) == 2
|
if not (isinstance(tree, list) and len(tree) == 2):
|
||||||
|
raise ValueError("compute_control_block: invalid tree node")
|
||||||
|
|
||||||
control_block = b""
|
control_block = b""
|
||||||
|
|
||||||
while isinstance(tree, list):
|
while isinstance(tree, list):
|
||||||
assert len(tree) == 2
|
if len(tree) != 2:
|
||||||
|
raise ValueError(f"expected binary branch, got {len(tree)} children")
|
||||||
sibling = tree[(path & 1) ^ 1]
|
sibling = tree[(path & 1) ^ 1]
|
||||||
tree = tree[(path & 1)]
|
tree = tree[(path & 1)]
|
||||||
control_block = compute_merkle_root(sibling) + control_block
|
control_block = compute_merkle_root(sibling) + control_block
|
||||||
path >>= 1
|
path >>= 1
|
||||||
|
|
||||||
assert isinstance(tree, dict)
|
if not isinstance(tree, dict):
|
||||||
|
raise ValueError("compute_control_block: path does not terminate at a leaf")
|
||||||
|
if len(control_block) > 32 * CONTROL_MAX_NODE_COUNT:
|
||||||
|
raise ValueError(
|
||||||
|
f"compute_control_block: {1 + len(control_block)} bytes exceeds the "
|
||||||
|
f"consensus limit of 1 + 32 * {CONTROL_MAX_NODE_COUNT}"
|
||||||
|
)
|
||||||
return bytes([tree["leafVersion"] | 1]) + control_block
|
return bytes([tree["leafVersion"] | 1]) + control_block
|
||||||
|
|
||||||
|
|
||||||
@@ -412,6 +432,39 @@ def run_single_test(v: Dict[str, Any], test_num: int) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def negative_structure_tests() -> None:
|
||||||
|
"""Malformed trees must raise -- never silently return a wrong root -- including under python -O."""
|
||||||
|
print("\nRunning negative structure tests...")
|
||||||
|
leaf_a = {"leafVersion": 0xC0, "script": "51"}
|
||||||
|
leaf_b = {"leafVersion": 0xC0, "script": "52"}
|
||||||
|
leaf_c = {"leafVersion": 0xC0, "script": "53"}
|
||||||
|
|
||||||
|
# A ternary branch must be refused, never reduced to its first two children.
|
||||||
|
try:
|
||||||
|
compute_merkle_root([leaf_a, leaf_b, leaf_c])
|
||||||
|
raise SystemExit("FAILED: ternary branch accepted")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# A leaf may sit at depth 128 (m = 128) but never deeper.
|
||||||
|
tree: ScriptTree = leaf_a
|
||||||
|
for _ in range(CONTROL_MAX_NODE_COUNT):
|
||||||
|
tree = [tree, leaf_b]
|
||||||
|
cb = compute_control_block(0, tree) # depth 128: still valid
|
||||||
|
if len(cb) != 1 + 32 * CONTROL_MAX_NODE_COUNT:
|
||||||
|
raise SystemExit("FAILED: wrong control block length at maximum depth")
|
||||||
|
|
||||||
|
tree = [tree, leaf_b] # depth 129: consensus-invalid
|
||||||
|
for attempt in (lambda: compute_merkle_root(tree), lambda: compute_control_block(0, tree)):
|
||||||
|
try:
|
||||||
|
attempt()
|
||||||
|
raise SystemExit("FAILED: >128-deep tree accepted")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("3/3 negative structure tests passed.")
|
||||||
|
|
||||||
|
|
||||||
def BIP360_tests() -> None:
|
def BIP360_tests() -> None:
|
||||||
"""Run all BIP-360 Test Vectors."""
|
"""Run all BIP-360 Test Vectors."""
|
||||||
print("\nRunning BIP-0360 Pay-to-Merkle-Root (P2MR) Tests...")
|
print("\nRunning BIP-0360 Pay-to-Merkle-Root (P2MR) Tests...")
|
||||||
@@ -421,6 +474,7 @@ def BIP360_tests() -> None:
|
|||||||
|
|
||||||
passed = sum(run_single_test(v, i + 1) for i, v in enumerate(test_vectors))
|
passed = sum(run_single_test(v, i + 1) for i, v in enumerate(test_vectors))
|
||||||
print(f"\n{passed}/{len(test_vectors)} BIP-360 tests passed successfully.")
|
print(f"\n{passed}/{len(test_vectors)} BIP-360 tests passed successfully.")
|
||||||
|
negative_structure_tests()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user