فهرست مقاله
۱. عضویتِ بایتها را با درستیِ محتوا یکی نگیریم

برای یک تصویرِ ثابت از سندهای دانش، گاهی میخواهیم عضویتِ یک سند را بدون فرستادنِ دوبارهٔ همهٔ سندها بررسی کنیم. درختِ مرکل این مسئله را با چکیدهٔ برگها و چند چکیدهٔ همراه صورتبندی میکند. این درس پنج رشتهٔ بایتیِ ساختگی میسازد؛ نه پایگاهِ دانشِ واقعی داریم و نه سامانهٔ جستوجو را تغییر میدهیم. هدف، وارسیِ یک شاهدِ عضویت نسبت به ریشهای ازپیشپذیرفتهشده است.
«سند» در این کد فقط bytes دقیق است. برنامه متن را استخراج، نرمالسازی یا به JSON تبدیل نمیکند؛ حتی بایتِ تهی مجاز است. اگر ورودیِ کاربرد از عنوان، بدنه و فراداده ساخته میشود، قراردادِ تبدیلِ آنها به بایت باید جدا مشخص شود. تغییرِ پایانِ خط یا فاصله میتواند ورودی را عوض کند؛ این برنامه همارزیِ معناییِ دو نوشته را تشخیص نمیدهد.
در درسِ manifest و نسخهٔ سازگارِ نمایه، رابطهٔ سند، قطعه و تنظیم کنترل میشد. اینجا آن کنترلها را تکرار نمیکنیم. همچنین گروهبندیِ دقیقِ سند با هش بایتِ کامل را برای تصمیمِ برابری مقایسه میکند؛ شاهدِ مرکل همهٔ بایتهای مجموعه را در اختیارِ بررسیکننده نمیگذارد و تصمیمِ حذف یا ادغامِ سند نیست.
تطبیقِ ریشه، درستیِ علمیِ متن، هویتِ نویسنده یا تازهبودنِ نسخه را ثابت نمیکند. تفسیرِ رمزنگاریِ تعهد به داده بر فرضهای امنیتیِ SHA-256، از جمله دشواریِ یافتنِ برخورد و پیشتصویرِ دوم، تکیه دارد؛ برابریِ هش را برهانِ ریاضیِ ناممکنبودنِ برخورد نمینامیم. اگر طرفِ مقابل اجازه داشته باشد ریشهٔ پذیرفتهشده را هم عوض کند، میتواند برای مجموعهٔ خودش شاهدِ سازگار بسازد.
۲. برگ، گره و شکلِ نامتوازنِ درخت

از تعریفِ درخت در بخشِ ۲٫۱٫۱ از RFC 9162 استفاده میکنیم و تابعِ هش را SHA-256 انتخاب میکنیم. ریشهٔ فهرستِ تهی، هشِ بایتِ تهی است. برای یک برگ، یک بایتِ صفر پیش از داده میآید؛ برای گرهِ داخلی، یک بایتِ یک پیش از چکیدهٔ چپ و سپس راست قرار میگیرد. اینها بایتهای صفر و یکاند، نه نویسههای متنیِ «00» و «01».
این جداسازیِ دامنه، ورودیِ برگ و گره را از هم متمایز میکند. مثلاً فهرستِ تهی با فهرستِ دارای یک سندِ تهی یکی نیست. چکیدههای فرزند طولِ ثابتِ سیودو بایت دارند؛ جای چپ و راست حفظ میشود و آنها را بر اساسِ مقدار مرتب نمیکنیم. ورودی نیز فهرستِ مرتبِ بایتهاست؛ سندهای تکراری حذف نمیشوند و جابهجاییِ دو سند، قراردادِ مجموعه را تغییر میدهد.
برای بیش از یک برگ، بزرگترین توانِ دو که از تعدادِ برگها کوچکتر است، مرزِ تقسیم میشود. پنج برگ به چهار و یک، و شش برگ به چهار و دو تقسیم میشوند. برگِ آخر را برای پُرکردنِ درخت تکرار نمیکنیم. شکل با تعدادِ برگها تعیین میشود؛ پس اندازهٔ درخت بخشی از ورودیِ بررسی است. شاهدهای موقعیتهای مختلف نیز الزاماً طولِ برابر ندارند.
در مستنداتِ hashlib پایتون، digest بایتهای چکیده و hexdigest نمایشِ متنیِ شانزدهشانزدهی را برمیگرداند. کد، فرزندان را با digest سیودوبایتی ترکیب میکند؛ متنِ شصتوچهارنویسهای فقط برای نمایشِ خروجی است. صفحهٔ خواندهشده نسخهٔ مستنداتِ ۳٫۱۴٫۸ را نشان میدهد و اجرای ثبتشدهٔ ما با پایتونِ ۳٫۱۴٫۵ انجام شده است.
۳. شاهدِ جایگاه و بازسازیِ ریشه

شاهد، چکیدهٔ زیرشاخههای خواهر یا برادر در راهِ برگ تا ریشه است. ترتیبِ آن از پایین به بالاست. در مثالِ پنجبرگی، برای برگِ شمارهٔ دو بهترتیب چکیدهٔ برگِ سه، زیرشاخهٔ صفر و یک، و برگِ چهار لازم میشود. شمارهها از صفر شروع میشوند. جهتِ ترکیب از شماره و اندازهٔ معلومِ درخت به دست میآید؛ شاهد پرچمِ دلخواهِ چپ و راست به برنامه تحمیل نمیکند.
تابعِ بررسی، ابتدا شکلِ مسیر و تعدادِ دقیقِ همراهان را پیدا میکند. بعد از هشِ برگ شروع میکند و همراهان را با جهتِ درست ترکیب میکند؛ همه باید دقیقاً مصرف شوند. مسیرِ ناقص یا اضافه خطای قرارداد است. شاهدِ دارای ساختارِ درست ولی ریشهٔ متفاوت، False میدهد. درختِ تکبرگی مسیرِ تهی دارد؛ برای درختِ بدون برگ، اصلاً جایگاهِ معتبرِ عضویت نداریم.
بخشِ کاملِ شاهدِ عضویت در RFC 9162 تولید و بررسیِ شاهد را شرح میدهد. برنامهٔ آموزشیِ حاضر جهتها را با بازکردنِ شکلِ درخت محاسبه میکند؛ بازنویسیِ کاملِ پروتکلِ CT یا قالبِ پیامهایش نیست. ریشه و اندازه باید از مرجعِ جداگانهٔ پذیرفتهشده بیایند؛ در این فایل امضا، گواهی، مهرِ زمانی یا دسترسی به دفترِ شبکه وجود ندارد.
۴. کدِ کامل و مرجعِ شکلِ مستقل

تابعِ merkle_root تاپلِ صفر تا هشت سند را میپذیرد؛ هر سند باید bytes دقیق با طولِ حداکثر شصتوچهار بایت باشد. inclusion_proof همان نوعِ تاپل را با یک تا هشت سند و شمارهٔ معتبر میگیرد. verify_inclusion یک سندِ bytes، اندازهٔ یک تا هشت و شمارهٔ معتبر میگیرد؛ ریشه و هر همراه باید bytes دقیقِ سیودوبایتی و مسیر تاپل باشد. اندازه و شماره int دقیقاند؛ بولی، رشته، bytearray و زیرکلاسِ نوعِ لازم پذیرفته نیستند.
"""Bounded ordered SHA256 Merkle inclusion; not a CT log or trust service."""
from functools import lru_cache
from hashlib import sha256
import json
import platform
def leaf_bytes(value):
if type(value) is not bytes or len(value) > 64:
raise ValueError('leaf_bytes')
return value
def entries(value):
if type(value) is not tuple or len(value) > 8:
raise ValueError('entries')
return tuple(leaf_bytes(v) for v in value)
def leaf_hash(value):
return sha256(b'\x00' + value).digest()
def node_hash(left, right):
return sha256(b'\x01' + left + right).digest()
def split(n):
return 1 << ((n - 1).bit_length() - 1)
def _root(values):
if not values:
return sha256(b'').digest()
if len(values) == 1:
return leaf_hash(values[0])
k = split(len(values))
return node_hash(_root(values[:k]), _root(values[k:]))
def merkle_root(values):
return _root(entries(values))
def location(size, index):
if type(size) is not int or not 1 <= size <= 8:
raise ValueError('tree_size')
if type(index) is not int or not 0 <= index < size:
raise ValueError('leaf_index')
def inclusion_proof(values, index):
values = entries(values)
location(len(values), index)
def visit(part, offset):
if len(part) == 1:
return ()
k = split(len(part))
if offset < k:
return visit(part[:k], offset) + (_root(part[k:]),)
return visit(part[k:], offset - k) + (_root(part[:k]),)
return visit(values, index)
def verify_inclusion(value, index, size, proof, accepted_root):
"""Malformed contract raises ValueError; well-formed mismatch is False.
accepted_root must come from a separately accepted reference. This function
neither authenticates it nor decides freshness, provenance, or content truth.
"""
value = leaf_bytes(value)
location(size, index)
if type(accepted_root) is not bytes or len(accepted_root) != 32:
raise ValueError('root_digest')
if type(proof) is not tuple or any(type(p) is not bytes or len(p) != 32 for p in proof):
raise ValueError('proof_digests')
# Derive orientations from the known tree shape, not from untrusted flags.
top_down = []
n, m = size, index
while n > 1:
k = split(n)
top_down.append(m < k)
if m < k:
n = k
else:
n, m = n - k, m - k
if len(proof) != len(top_down):
raise ValueError('proof_length')
current = leaf_hash(value)
for sibling, current_is_left in zip(proof, reversed(top_down), strict=True):
current = (node_hash(current, sibling) if current_is_left
else node_hash(sibling, current))
return current == accepted_root
@lru_cache(None)
def all_ordered_shapes(n):
"""Every full ordered binary shape with n leaves; no production split."""
if n == 1:
return (None,)
return tuple((a, b) for left_count in range(1, n)
for a in all_ordered_shapes(left_count)
for b in all_ordered_shapes(n - left_count))
def shape_info(shape):
"""Leaf count, perfectness, and a structural left-complete predicate."""
if shape is None:
return 1, True, True
a, ap, ac = shape_info(shape[0])
b, bp, bc = shape_info(shape[1])
return a + b, ap and bp and a == b, ap and b <= a and bc
def shape_oracle(values):
"""Brute shape selection plus separately streamed hashing/proof paths.
A canonical internal node has a perfect left subtree and a no-larger
canonical right subtree. Uniqueness is checked among every ordered shape.
"""
if not values:
return sha256().digest(), (), 0
candidates = all_ordered_shapes(len(values))
choices = [s for s in candidates if shape_info(s)[2]]
assert len(choices) == 1
def evaluate(shape, start):
if shape is None:
h = sha256(); h.update(bytes([0])); h.update(values[start])
return h.digest(), {start: ()}
count = shape_info(shape[0])[0]
left, lp = evaluate(shape[0], start)
right, rp = evaluate(shape[1], start + count)
h = sha256(); h.update(bytes([1])); h.update(left); h.update(right)
return h.digest(), ({i: p + (right,) for i, p in lp.items()} |
{i: p + (left,) for i, p in rp.items()})
root, witnesses = evaluate(choices[0], 0)
return root, tuple(witnesses[i] for i in range(len(values))), len(candidates)
def rejects(call, message):
try:
call()
except ValueError as error:
return str(error) == message
return False
def flip(value):
return bytes([value[0] ^ 1]) + value[1:]
def self_check():
values = (b'doc:alpha', b'doc:beta', b'doc:gamma', b'doc:delta', b'doc:epsilon')
index = 2
root, proof = merkle_root(values), inclusion_proof(values, index)
checks = {}
checks['empty_tree_is_not_one_empty_leaf_and_sha256_size_is_32_bytes'] = (
merkle_root(()) == sha256(b'').digest() and
merkle_root((b'',)) == sha256(b'\x00').digest() and
merkle_root(()) != merkle_root((b'',)) and len(root) == 32)
left = sha256(b'\x00A').digest(); right = sha256(b'\x00B').digest()
checks['two_leaf_manual_concatenation_and_oriented_sibling_proofs'] = (
merkle_root((b'A', b'B')) == sha256(b'\x01' + left + right).digest() and
inclusion_proof((b'A', b'B'), 0) == (right,) and
inclusion_proof((b'A', b'B'), 1) == (left,))
checks['five_leaf_tree_splits_four_plus_one_without_duplicate_padding'] = (
root == node_hash(merkle_root(values[:4]), leaf_hash(values[4])) and
proof == (leaf_hash(values[3]), merkle_root(values[:2]), leaf_hash(values[4])) and
[len(inclusion_proof(values, i)) for i in range(5)] == [3, 3, 3, 3, 1] and
merkle_root(values) != merkle_root(values + values[-1:]))
oracle_ok, verification_ok, cases, memberships, shape_counts = True, True, 0, 0, []
for size in range(9):
for variant in range(2):
row = tuple(bytes([i, variant]) if variant == 0 else b'same' for i in range(size))
expected, paths, count = shape_oracle(row)
oracle_ok &= merkle_root(row) == expected
for i in range(size):
actual = inclusion_proof(row, i)
oracle_ok &= actual == paths[i]
verification_ok &= verify_inclusion(row[i], i, size, actual, expected)
memberships += 1
cases += 1
if variant == 0:
shape_counts.append(count)
checks['18_bounded_trees_match_unique_bruteforce_shape_root_and_proof_oracle'] = bool(
oracle_ok and cases == 18 and shape_counts == [0, 1, 1, 2, 5, 14, 42, 132, 429])
checks['72_honest_memberships_verify_including_duplicates_and_unbalanced_sizes'] = bool(
verification_ok and memberships == 72 and
verify_inclusion(b'', 0, 1, (), merkle_root((b'',))))
checks['changed_leaf_and_changed_accepted_root_are_rejected_as_well_formed_mismatches'] = (
not verify_inclusion(b'changed', index, 5, proof, root) and
not verify_inclusion(values[index], index, 5, proof, flip(root)))
checks['each_same_length_sibling_bit_mutation_breaks_this_fixture_proof'] = all(
not verify_inclusion(values[index], index, 5, proof[:i] + (flip(proof[i]),) + proof[i+1:], root)
for i in range(len(proof)))
checks['sibling_order_and_same_depth_wrong_index_are_checked'] = (
not verify_inclusion(values[index], index, 5, tuple(reversed(proof)), root) and
not verify_inclusion(values[index], 1, 5, proof, root))
checks['proof_nodes_are_consumed_exactly_and_single_leaf_requires_empty_path'] = (
rejects(lambda: verify_inclusion(values[index], index, 5, proof[:-1], root), 'proof_length') and
rejects(lambda: verify_inclusion(values[index], index, 5, proof + (root,), root), 'proof_length') and
rejects(lambda: verify_inclusion(b'A', 0, 1, (root,), merkle_root((b'A',))), 'proof_length'))
checks['order_and_multiplicity_are_not_deduplicated_and_equal_bytes_can_occupy_two_positions'] = (
merkle_root((b'A', b'B')) != merkle_root((b'B', b'A')) and
merkle_root((b'A',)) != merkle_root((b'A', b'A')) and
all(verify_inclusion(b'A', i, 2, inclusion_proof((b'A', b'A'), i),
merkle_root((b'A', b'A'))) for i in range(2)))
altered = values[:index] + (b'false-claim',) + values[index+1:]
altered_root, altered_proof = merkle_root(altered), inclusion_proof(altered, index)
checks['accepting_a_supplier_replaced_root_cannot_authenticate_the_document'] = (
verify_inclusion(altered[index], index, 5, altered_proof, altered_root) and
not verify_inclusion(altered[index], index, 5, altered_proof, root))
class BytesSubclass(bytes):
pass
class IntSubclass(int):
pass
checks['entry_tuple_and_exact_leaf_byte_bounds_are_explicit'] = (
len(merkle_root((b'x' * 64,) * 8)) == 32 and
all(rejects(lambda bad=bad: merkle_root(bad), 'entries')
for bad in ([], None, b'A', (b'A',) * 9)) and
all(rejects(lambda bad=bad: merkle_root((bad,)), 'leaf_bytes')
for bad in ('A', bytearray(b'A'), True, 1, b'A' * 65, BytesSubclass(b'A'))))
checks['size_index_root_and_path_item_types_are_validated_before_hashing'] = (
all(rejects(lambda n=n: verify_inclusion(b'A', 0, n, (), root), 'tree_size')
for n in (0, 9, True, 1.0, IntSubclass(1))) and
all(rejects(lambda i=i: verify_inclusion(b'A', i, 5, proof, root), 'leaf_index')
for i in (-1, 5, True, 1.0, IntSubclass(1))) and
all(rejects(lambda r=r: verify_inclusion(b'A', 0, 1, (), r), 'root_digest')
for r in (root.hex(), b'', root[:-1], bytearray(root), BytesSubclass(root))) and
all(rejects(lambda p=p: verify_inclusion(b'A', 0, 2, p, root), 'proof_digests')
for p in ([root], (root.hex(),), (root[:-1],), (bytearray(root),))))
assert len(checks) == 13 and all(type(v) is bool for v in checks.values())
assert all(checks.values()), checks
return dict(python=platform.python_version(), dependencies='standard library only',
scope='Ordered exact bytes/SHA256 educational inclusion, not CT protocol, signatures, content truth or freshness',
leaf_limit_bytes=64, tree_limit_leaves=8, digest_bytes=32,
fixture=dict(leaf_bytes_ascii=[v.decode('ascii') for v in values], tree_size=5, leaf_index=index,
root_sha256_hex=root.hex(), sibling_hashes_bottom_up=[p.hex() for p in proof],
proof_digest_bytes=sum(map(len, proof)), verified=verify_inclusion(values[index], index, 5, proof, root),
all_index_proof_lengths=[len(inclusion_proof(values, i)) for i in range(5)]),
oracle_tree_cases=cases, honest_memberships=memberships, all_ordered_shape_counts_by_size=shape_counts,
source_authentication_performed=False, consistency_or_append_only_proof_performed=False,
unique_executed_checks=len(checks), checks=checks, all_passed=all(checks.values()))
if __name__ == '__main__':
print(json.dumps(self_check(), ensure_ascii=False, indent=2))
{
"python": "3.14.5",
"dependencies": "standard library only",
"scope": "Ordered exact bytes/SHA256 educational inclusion, not CT protocol, signatures, content truth or freshness",
"leaf_limit_bytes": 64,
"tree_limit_leaves": 8,
"digest_bytes": 32,
"fixture": {
"leaf_bytes_ascii": [
"doc:alpha",
"doc:beta",
"doc:gamma",
"doc:delta",
"doc:epsilon"
],
"tree_size": 5,
"leaf_index": 2,
"root_sha256_hex": "bdac1c2cd248a01babcc8eda77d1123dac644b89e69ba67c6ae78d51c8990d2a",
"sibling_hashes_bottom_up": [
"37925983b069f0165848e2fd183e5f54b9427c8996de056f74c92d6ec26babb8",
"2d6fafc95ce51fba70a3349b6f0379b60b49fd423554cb580806e30b15ed8ce0",
"88a8485a5a14cd20d8ff54a21ca596c7ee0db5aa31e1ab9d1b86e7a9c61b2c5b"
],
"proof_digest_bytes": 96,
"verified": true,
"all_index_proof_lengths": [
3,
3,
3,
3,
1
]
},
"oracle_tree_cases": 18,
"honest_memberships": 72,
"all_ordered_shape_counts_by_size": [
0,
1,
1,
2,
5,
14,
42,
132,
429
],
"source_authentication_performed": false,
"consistency_or_append_only_proof_performed": false,
"unique_executed_checks": 13,
"checks": {
"empty_tree_is_not_one_empty_leaf_and_sha256_size_is_32_bytes": true,
"two_leaf_manual_concatenation_and_oriented_sibling_proofs": true,
"five_leaf_tree_splits_four_plus_one_without_duplicate_padding": true,
"18_bounded_trees_match_unique_bruteforce_shape_root_and_proof_oracle": true,
"72_honest_memberships_verify_including_duplicates_and_unbalanced_sizes": true,
"changed_leaf_and_changed_accepted_root_are_rejected_as_well_formed_mismatches": true,
"each_same_length_sibling_bit_mutation_breaks_this_fixture_proof": true,
"sibling_order_and_same_depth_wrong_index_are_checked": true,
"proof_nodes_are_consumed_exactly_and_single_leaf_requires_empty_path": true,
"order_and_multiplicity_are_not_deduplicated_and_equal_bytes_can_occupy_two_positions": true,
"accepting_a_supplier_replaced_root_cannot_authenticate_the_document": true,
"entry_tuple_and_exact_leaf_byte_bounds_are_explicit": true,
"size_index_root_and_path_item_types_are_validated_before_hashing": true
},
"all_passed": true
}
مرجعِ مستقل، همهٔ شکلهای درختِ دودوییِ مرتب را تولید میکند. سپس تنها شکلی را نگه میدارد که در هر گره، چپ کامل و راستِ معتبر از چپ بزرگتر نباشد. یکتاییِ انتخاب را نیز کنترل میکند. برای هشت برگ، چهارصدوبیستونه شکل نامزد داریم؛ این روش به تابعِ تقسیم یا ریشهٔ اصلی متکی نیست. هشکردنِ مرجع جدا و با update انجام میشود، اما خودِ SHA-256 را دوباره پیاده نمیکنیم.
یک اجرای کامل، سیزده گروهِ بررسی را گذرانده است. هجده حالتِ درختِ محدود و هفتادودو عضویت در خانوادهها بررسی میشوند؛ هر حالت، گروهِ تازهای نیست. بیتگردانیِ انتخابیِ همراهان، برگِ عوضشده، ریشهٔ عوضشده و جابهجاییِ همراهان روی همین شاهدها رد میشوند؛ این مشاهده، اثباتِ ردِ هر دستکاریِ ممکن نیست. مثال آفلاین است و ادعای سرعت، مصرفِ حافظه یا آمادگیِ سرویسِ عملیاتی ندارد.
۵. چهار پرسش دربارهٔ اعتماد و ورودیِ مرزی

پرسش اول: چرا شاهدِ برگِ شمارهٔ چهار کوتاهتر است؟ پاسخ: در پنج برگ، جایگاهِ چهار مستقیماً فرزندِ راستِ ریشه است و تنها چکیدهٔ چهار برگِ نخست را لازم دارد. طولِ شاهدها بهترتیب سه، سه، سه، سه و یک است. شاهدِ جایگاهِ دو نودوشش بایتِ چکیده دارد؛ این عدد شاملِ سند، شماره، اندازه، قالبِ انتقال یا کلِ حافظهٔ برنامه نیست.
پرسش دوم: آیا بایتِ برابر، منشأ یا جایگاهِ یکتا را ثابت میکند؟ پاسخ: خیر؛ دو برگ میتوانند هر دو A باشند و شاهدِ معتبرِ موقعیتِ خود را داشته باشند. داده، اندازه و شماره با هم بررسی میشوند؛ خودِ A شناسهٔ یکتای رخدادِ ورود نیست. اگر ردیابیِ منشأ لازم است، آن فراداده و قالبِ تعهد باید جدا طراحی و بررسی شود.
پرسش سوم: اگر فرستنده هم سند و هم ریشه را عوض کند چه میشود؟ پاسخ: آزمون عمداً چنین مجموعهای میسازد؛ شاهد با ریشهٔ تازه میگذرد، ولی با ریشهٔ قبلی رد میشود. پس بررسیِ عضویت، سازوکارِ اعتماد به ریشه نیست. پذیرشِ ریشه، هویتِ مرجع و سیاستِ نسخهٔ مجاز، مسئلهای بیرون از این تابعاند؛ صحتِ یک محاسبه آنها را فراهم نمیکند.
پرسش چهارم: آیا یک شاهد، فقطافزودنیبودنِ نسخهٔ بعدی را ثابت میکند؟ پاسخ: خیر؛ اینجا شاهدِ سازگاریِ دو ریشه را محاسبه نمیکنیم و دربارهٔ تاریخچه یا تازگی نتیجهای نداریم. RFC 9162 سندِ آزمایشیِ دسامبرِ ۲۰۲۱ است؛ تاریخِ دسترسیِ ۲۷ مهرِ ۱۴۰۵ خبرِ تازه محسوب نمیشود. نتیجهٔ این درس به عضویتِ بایتِ مشخص، در شکل و ریشهٔ پذیرفتهشدهٔ مشخص محدود میماند.
منابع و مطالعهٔ بیشتر
- RFC9162؛ تعریفِ درخت و تولید و بررسیِ شاهدِ عضویت، بخشهای ۲٫۱٫۱ تا ۲٫۱٫۳
- Python3.14؛ SHA-256 و بایتِ digest در hashlib
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

