> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# المحافظ والمفاتيح: العبارة السرية وكيف تحمي أصولك
- URL: https://arabroot.io/articles/المحافظ-والمفاتيح-العبارة-السرية-وكيف-تحمي-اصولك/
- Published: 2026-10-28T06:06:35.000Z
- Updated: 2026-10-28T07:37:35.000Z
- Description: ما الذي تحفظه المحفظة فعلاً، وكيف تولد اثنتا عشرة كلمة كل مفاتيحك، وكيف تفصل شركة صغيرة بين معرفة الأرصدة والتوقيع.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, البلوكتشين والعملات الرقمية, الاستضافة الذاتية

لنفرض أنك انضممت كقائد تقني إلى شركة ناشئة تبني تطبيقات على شبكة Ethereum لعملاء خارج المملكة، وفي أسبوعك الأول تطلب من الفريق قائمة بكل المفاتيح التي تملكها الشركة. فتكتشف أن المفتاح الذي ينشر العقود الذكية على الشبكة الرئيسية، ومعه رصيد يدفع منه الفريق رسوم الغاز، محفوظ كعبارة من 12 كلمة في ملف `.env` داخل مستودع Git، وأن المطور الذي أنشأه غادر الشركة قبل شهرين، وأن نسخة أخرى من العبارة موجودة في صورة على هاتف المدير التنفيذي «للاحتياط». وعندما تسأل من يستطيع أن يصرف من هذا الرصيد، تكون الإجابة الصادقة: «كل من رأى هذا الملف في أي وقت». وهذه ليست قصة نادرة، وإنما هي الطريقة التي يبدأ بها كثير من الفرق، لأن أحداً لم يشرح لهم ما هي المحفظة فعلاً، وما الذي تحميه العبارة السرية، ولماذا تختلف عن كلمة المرور.

وفي [قسم الأمان في مقال البلوكتشين للمبتدئين](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/#security) وضعنا القواعد الأساسية باختصار، وفي مقال التشفير في البلوكتشين رأينا كيف يتحول المفتاح الخاص إلى عنوان. أما هنا فسوف نبني محفظة من الصفر كما تبنيها المحافظ الحقيقية: من رقم عشوائي إلى اثنتي عشرة كلمة، ومن الكلمات إلى شجرة كاملة من المفاتيح، ثم نتحقق من كودنا بمتجهات الاختبار الرسمية وبعبارة Anvil المعروفة وبعقدة Bitcoin Core، ونبني لشركتنا محفظة مشاهدة على السيرفر لا تحمل أي مفتاح خاص، وموقعاً منفصلاً يوقع بصيغة PSBT، ونجرب خطأً شائعاً يكشف الفرع كله من مفتاح واحد. والمقال تعليمي تقني، ولا يتضمن أي نصيحة استثمارية، ولا يوصي بأي منتج أو مزود، وكل المفاتيح فيه مفاتيح اختبار.

وسوف نناقش في هذا المقال ما يلي:

- ما الذي تحفظه المحفظة فعلاً، والفرق بين الحفظ الذاتي Self-custody والحفظ لدى طرف آخر Custodial.
- لماذا جاءت العبارة السرية: من المفاتيح المنفصلة إلى المحافظ الهرمية HD Wallets، ثم BIP39.
- كيف تتحول العشوائية إلى كلمات ثم إلى بذرة Seed، ومجموع التحقق، والعبارة الإضافية Passphrase.
- شجرة المفاتيح BIP32 والمسارات BIP44 وBIP84، مع تجربة نطابق فيها عناوين Anvil وعقدة Bitcoin Core.
- محفظة مشاهدة Watch-only على السيرفر وتوقيع منفصل بصيغة PSBT، بتجربة كاملة.
- نجرب العطل: المفتاح العام الموسع xpub مع مفتاح ابن واحد يكشف الفرع كله، ولماذا توجد المستويات المقواة Hardened.
- محافظ العتاد Hardware Wallets والنسخ الاحتياطية، والتوقيع المتعدد Multisig، وحادثة تسرب بيانات Ledger سنة 2020.
- ما الذي توقع عليه في Ethereum: المعاملات والرسائل والموافقات، واستمرارية الوصول إذا غاب صاحب المفتاح.
- سياسة مفاتيح مكتوبة لشركة صغيرة، وقائمة تحقق.

## ما الذي تحفظه المحفظة فعلاً؟

أول تصحيح تحتاجه في الشركة أن المحفظة لا تحفظ عملات. فالعملات في Bitcoin قطع غير منفقة على السلسلة كما شرحنا في مقال كيف يعمل Bitcoin، والأرصدة في Ethereum قيم في حالة الشبكة، وكلاهما موجود عند كل عقدة في العالم، والمحفظة لا تملك نسخة خاصة منه. ما تحفظه المحفظة هو المفاتيح الخاصة التي تستطيع أن توقع معاملات تحرك هذه القيم، أو العبارة السرية التي تولد كل هذه المفاتيح، ومعها برنامج يبني المعاملات ويحسب الرسوم ويعرض الأرصدة. لذلك «سرقة المحفظة» تعني في الحقيقة سرقة المفاتيح، و«استعادة المحفظة» تعني إعادة توليد المفاتيح من العبارة على أي جهاز، حتى لو احترق الجهاز الأول.

ومن هنا يأتي التقسيم الأهم في هذا المجال: إذا كنت تملك المفاتيح بنفسك فهذا حفظ ذاتي Self-custody، فلا يستطيع أحد أن يجمد أموالك أو يخسرها نيابة عنك، وفي المقابل لا يستطيع أحد أن يعيدها إليك إذا أضعت المفاتيح. وإذا كانت منصة أو شركة تملك المفاتيح نيابة عنك فهذا حفظ لدى طرف آخر Custodial، وأنت تملك رقماً في قاعدة بياناتها ووعداً بالسحب، وهذا بالضبط ما رأيناه في انهيار Mt. Gox وFTX في مقال تاريخ العملات الرقمية. والصورة التالية تبين الفكرة:

![مخطط يبين أن العملات على السلسلة كقطع مقفلة يراها الجميع، وأن المحفظة تحفظ المفاتيح الخاصة أو العبارة السرية وبرنامجاً يبني المعاملات ويوقعها، وتحته بطاقتان: الحفظ الذاتي حيث تملك المفاتيح فلا يجمد أحد أموالك ولا يعيدها أحد إذا أضعتها، والحفظ لدى طرف آخر حيث تملك المنصة المفاتيح وتملك أنت رقماً في قاعدة بياناتها](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-01-wallet.webp)

المحفظة تحفظ مفاتيح، والعملات على السلسلة، ومن يملك المفتاح يملك القرار

وفي قصتنا، المفتاح الذي ينشر العقود يملكه فعلياً كل من رأى ملف `.env`، أي أن الشركة اختارت دون أن تقصد نموذجاً أسوأ من الحفظ الذاتي ومن الحفظ لدى طرف آخر معاً: لا يوجد مالك واحد مسؤول، ولا يوجد طرف خارجي مسؤول. والحل لا يبدأ بشراء جهاز، وإنما بفهم ما الذي تولده العبارة، وما الذي يجب أن يبقى سراً، وما الذي يمكن أن تنشره. وسوف نبني هذا الفهم خطوة خطوة، ثم نعود في آخر المقال إلى الشركة نفسها ونكتب لها سياسة مفاتيح كاملة، بنداً بنداً، مع سبب كل بند.

## لماذا جاءت العبارة السرية؟

في المحافظ الأولى كان كل عنوان مفتاحاً خاصاً عشوائياً مستقلاً، والمحفظة ملف يحفظ قائمة هذه المفاتيح. والمشكلة أن المحفظة الجيدة تولد عنواناً جديداً لكل استلام ولكل مخرج باقي للخصوصية كما شرحنا في مقال كيف يعمل Bitcoin، فإذا أخذت نسخة احتياطية من الملف اليوم، ثم ولدت المحفظة عشرة مفاتيح جديدة غداً، فنسختك لا تحتوي هذه المفاتيح، وأي مبلغ وصل إليها يضيع إذا تعطل الجهاز. والحل البسيط أن تولد المحفظة مسبقاً مخزوناً من المفاتيح وتأخذ نسخة كلما استهلكت جزءاً منه، وهذا يعمل، ولكنه يجعل النسخ الاحتياطي عملية متكررة ينساها الناس.

والحل الأفضل جاء في [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki?ref=arabroot.io)، وهو المحافظ الهرمية الحتمية Hierarchical Deterministic Wallets: كل المفاتيح تشتق بطريقة محددة من سر واحد يسمى البذرة Seed، فتكفي نسخة واحدة من البذرة لتعيد توليد كل مفتاح ستستخدمه المحفظة في حياتها كلها. ولكن البذرة 64 بايت من الأرقام، ونسخها يدوياً على ورقة يسبب أخطاء، فجاء [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki?ref=arabroot.io) بطريقة لتمثيل العشوائية التي تنتج البذرة كقائمة كلمات من قائمة ثابتة فيها 2048 كلمة، يسهل على الإنسان أن يكتبها ويقرأها، ومعها مجموع تحقق يكتشف أغلب الأخطاء. وهذه الكلمات هي ما يسمى العبارة السرية أو عبارة الاسترداد Recovery Phrase أو Seed Phrase.

وقد يتساءل البعض: إذا كانت العبارة تولد كل شيء، أليست كلمة مرور طويلة؟ والإجابة أنها تختلف عن كلمة المرور في نقطة جوهرية: كلمة المرور تحمي حساباً عند جهة تستطيع أن تعيد تعيينها، وتستطيع أن توقف من يخمنها بعد محاولات قليلة، أما العبارة فهي السر نفسه، ومن يملكها يولد المفاتيح على جهازه دون أن يسأل أحداً، ولا توجد جهة تستطيع أن تغيرها أو توقفه. لذلك كل قاعدة تعرفها عن كلمات المرور القوية لا تكفي هنا، والقاعدة الصحيحة أن العبارة لا تكتب أبداً في أي نظام متصل بالإنترنت.

## من العشوائية إلى الكلمات إلى البذرة

الخطوات في BIP39 خمس، والصورة التالية تبينها بقيم متجه الاختبار الرسمي الأول:

![خمس خطوات: عشوائية من 128 بت، ثم مجموع تحقق من أول 4 بتات من SHA-256 فيصبح المجموع 132 بت أي 12 كلمة من 11 بت، ثم الكلمات abandon abandon حتى about، ثم البذرة من PBKDF2-HMAC-SHA512 بـ 2048 دورة وملح mnemonic مع العبارة الإضافية، ثم المفتاح الرئيسي من HMAC-SHA512 بمفتاح Bitcoin seed، وتحتها بطاقتان: كلمة خاطئة واحدة تكتشف في الغالب ولكن واحداً من كل 16 خطأ يمر، والعبارة الإضافية تعطي محفظة مختلفة تماماً دون أي رسالة خطأ](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-02-bip39.webp)

العبارة تمثيل للعشوائية بكلمات، والبذرة تأتي من العبارة بدالة بطيئة عمداً

1. **العشوائية Entropy**: رقم عشوائي من 128 بت لاثنتي عشرة كلمة، أو 256 بت لأربع وعشرين، من مولد آمن للتشفير، وكل أمان المحفظة يأتي من هذه الخطوة.
2. **مجموع التحقق Checksum**: أول بتات من SHA-256 للعشوائية، عددها طول العشوائية مقسوماً على 32، أي 4 بتات لاثنتي عشرة كلمة، تضاف إلى نهايتها.
3. **الكلمات**: تقسيم الناتج إلى أجزاء من 11 بت، وكل جزء رقم بين 0 و2047 يشير إلى كلمة في القائمة الرسمية، فتخرج 12 كلمة من 132 بت.
4. **البذرة Seed**: الكلمات تدخل في دالة PBKDF2-HMAC-SHA512 بـ 2048 دورة، والملح Salt هو كلمة `mnemonic` مع العبارة الإضافية Passphrase إن وجدت، فتخرج 64 بايت. والدورات الكثيرة تجعل كل تخمين أبطأ على من يحاول تجربة عبارات كثيرة.
5. **المفتاح الرئيسي Master Key**: HMAC-SHA512 للبذرة بالمفتاح النصي `Bitcoin seed`، فيكون النصف الأول المفتاح الخاص الرئيسي، والنصف الثاني رمز السلسلة Chain Code الذي يدخل في اشتقاق كل الأبناء.

### من أين تأتي العشوائية؟

كل ما في المحفظة يتوقف على الخطوة الأولى، فإذا كانت العشوائية ضعيفة فكل ما بعدها من دوال قوية لا ينفع، لأن المهاجم لا يحتاج أن يعكس الحساب، وإنما يكفيه أن يجرب العشوائيات الممكنة القليلة. والمصدر الصحيح هو مولد الأرقام العشوائية الآمن في نظام التشغيل، والذي يجمع عشوائيته من مصادر في العتاد، وهو ما تستخدمه وحدة `secrets` في Python التي استخدمناها في التجربة. أما الدالة `random` في Python فمصممة للمحاكاة والألعاب، ومخرجها يمكن توقعه بعد أن ترى عدداً كافياً منه، فلا تستخدمها في أي شيء يتعلق بالمفاتيح.

وقد رأينا في مقال التشفير في البلوكتشين التنبيه الذي نشره bitcoin.org سنة 2013 عن ضعف في مولد الأرقام العشوائية في Android جعل المحافظ التي أنشئت عليه معرضة للسرقة، وهذا مثال على أن العشوائية قد تضعف في مستوى لا يراه مطور التطبيق أصلاً. والخطأ الآخر الذي يقع فيه الناس هو أن يخترعوا العشوائية بأنفسهم: جملة من كتاب، أو كلمة مرور يحفظونها، ثم يحولونها إلى مفتاح بدالة هاش. وهذه الطريقة سيئة جداً، لأن الجمل التي يختارها البشر قليلة مقارنة بمساحة المفاتيح، ويستطيع من يملك قوائم الجمل والكلمات الشائعة أن يحسب مفاتيحها مسبقاً ويراقب عناوينها، فأي مبلغ يصلها يسحب. لذلك القاعدة بسيطة: العشوائية يولدها الجهاز الذي سيحفظ المفتاح، بمولد آمن، ولا يختارها إنسان.

### تجربة: نكتب BIP39 وBIP32 ونتحقق منهما

الملف التالي يكتب الخطوات كلها بلغة Python، ويحتاج قائمة الكلمات الرسمية وملف المنحنى `ec.py` الذي كتبناه في مقال التشفير في البلوكتشين، ومكتبة `pycryptodome` لدالتي Keccak وRIPEMD-160 فقط. حمل القائمة والمتجهات الرسمية التي يشير إليها BIP39 نفسه، وهي في مستودع [python-mnemonic](https://github.com/trezor/python-mnemonic?ref=arabroot.io):

```bash
curl -sL -o english.txt https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt
curl -sL -o vectors.json https://raw.githubusercontent.com/trezor/python-mnemonic/master/vectors.json
wc -l english.txt
```

والمخرج سوف يكون كما يلي:

```text
2048 english.txt
```

ثم احفظ الملف التالي باسم `hd.py`:

```python
# BIP39 (words -> seed) and BIP32 (seed -> tree of keys), written out to see every step. For learning only.
import hashlib, hmac, secrets
from Crypto.Hash import keccak, RIPEMD160
from ec import n, G, mul, add, p

WORDS = open("english.txt").read().split()          # the official BIP39 English list, 2048 words

def entropy_to_words(ent: bytes):
    bits = bin(int.from_bytes(ent, "big"))[2:].zfill(len(ent) * 8)
    cs = bin(hashlib.sha256(ent).digest()[0])[2:].zfill(8)[: len(ent) * 8 // 32]   # checksum: first ENT/32 bits of SHA-256
    allbits = bits + cs
    return [WORDS[int(allbits[i:i + 11], 2)] for i in range(0, len(allbits), 11)]

def words_valid(words):
    allbits = "".join(bin(WORDS.index(w))[2:].zfill(11) for w in words)
    ent_len = len(allbits) * 32 // 33
    ent = int(allbits[:ent_len], 2).to_bytes(ent_len // 8, "big")
    return allbits[ent_len:] == bin(hashlib.sha256(ent).digest()[0])[2:].zfill(8)[: ent_len // 32]

def words_to_seed(words, passphrase=""):
    return hashlib.pbkdf2_hmac("sha512", " ".join(words).encode(), ("mnemonic" + passphrase).encode(), 2048)

def ser_pub(P):                                      # compressed public key, 33 bytes
    return bytes([2 + (P[1] & 1)]) + P[0].to_bytes(32, "big")

def master(seed):
    I = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
    return int.from_bytes(I[:32], "big"), I[32:]     # (private key, chain code)

def child_priv(k, c, i):
    if i >= 0x80000000:                              # hardened: uses the PRIVATE key
        data = b"\x00" + k.to_bytes(32, "big") + i.to_bytes(4, "big")
    else:                                            # normal: uses the PUBLIC key
        data = ser_pub(mul(k)) + i.to_bytes(4, "big")
    I = hmac.new(c, data, hashlib.sha512).digest()
    return (int.from_bytes(I[:32], "big") + k) % n, I[32:]

def child_pub(P, c, i):                              # only possible for normal (non-hardened) children
    I = hmac.new(c, ser_pub(P) + i.to_bytes(4, "big"), hashlib.sha512).digest()
    return add(mul(int.from_bytes(I[:32], "big")), P), I[32:]

def derive(seed, path):
    k, c = master(seed)
    for part in path.split("/")[1:]:
        i = int(part.rstrip("'h")) + (0x80000000 if part[-1] in "'h" else 0)
        k, c = child_priv(k, c, i)
    return k, c

B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def b58check(b):
    b += hashlib.sha256(hashlib.sha256(b).digest()).digest()[:4]
    num, out = int.from_bytes(b, "big"), ""
    while num: num, r = divmod(num, 58); out = B58[r] + out
    return "1" * (len(b) - len(b.lstrip(b"\x00"))) + out

def ser_ext(version, depth, fpr, idx, c, keydata):
    return b58check(bytes.fromhex(version) + bytes([depth]) + fpr + idx.to_bytes(4, "big") + c + keydata)

def hash160(b): return RIPEMD160.new(hashlib.sha256(b).digest()).digest()
def eth_address(k):
    P = mul(k); h = keccak.new(digest_bits=256); h.update(P[0].to_bytes(32, "big") + P[1].to_bytes(32, "big"))
    return "0x" + h.hexdigest()[-40:]
```

في الكود أعلاه لاحظ التالي:

- الدالة `child_priv` لها فرعان: الاشتقاق المقوى Hardened للأرقام من `0x80000000` فما فوق يدخل فيه المفتاح الخاص للأب، والاشتقاق العادي يدخل فيه المفتاح العام للأب، وهذا الفرق الصغير هو موضوع تجربة العطل في آخر المقال.
- الدالة `child_pub` تشتق المفتاح العام للابن من المفتاح العام للأب ورمز السلسلة فقط، دون أي مفتاح خاص، وعلى هذه الخاصية تقوم محافظ المشاهدة.
- الدالة `ser_ext` تكتب المفتاح الموسع Extended Key بالصيغة المعروفة: أربعة بايتات للإصدار تحدد هل هو خاص أو عام ولأي شبكة، ثم العمق، وبصمة الأب، ورقم الابن، ورمز السلسلة، والمفتاح، ثم ترميز Base58Check الذي شرحناه في مقال الترميز.

والملف الثاني يتحقق من الكود بطريقتين مستقلتين: متجه BIP39 الرسمي الأول، وعبارة Anvil المعروفة التي تولد حسابات الاختبار في كل تجارب Ethereum على موقعنا. احفظه باسم `check_vectors.py`:

```python
# 1) our code against the official BIP39 vector, 2) against anvil's well-known test mnemonic.
import json
from hd import *

ent, words, seed_hex, xprv = json.load(open("vectors.json"))["english"][0]
w = entropy_to_words(bytes.fromhex(ent))
seed = words_to_seed(w, "TREZOR")                    # the vectors use the passphrase "TREZOR"
k, c = master(seed)
print("words match vector :", " ".join(w) == words)
print("seed matches vector:", seed.hex() == seed_hex)
print("xprv matches vector:", ser_ext("0488ade4", 0, b"\0" * 4, 0, c, b"\0" + k.to_bytes(32, "big")) == xprv)

anvil = "test test test test test test test test test test test junk".split()
print("anvil mnemonic valid:", words_valid(anvil))
for i in range(2):
    k, _ = derive(words_to_seed(anvil), f"m/44'/60'/0'/0/{i}")
    print(f"m/44'/60'/0'/0/{i} ->", eth_address(k))
```

```bash
docker run --rm --name lab-wallet-py -e PIP_DISABLE_PIP_VERSION_CHECK=1 -v "$PWD:/lab" -w /lab python:3.13.16-slim \
  bash -c "pip install -q --root-user-action=ignore pycryptodome==3.23.0 && python check_vectors.py"
```

والمخرج سوف يكون كما يلي:

```text
words match vector : True
seed matches vector: True
xprv matches vector: True
anvil mnemonic valid: True
m/44'/60'/0'/0/0 -> 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
m/44'/60'/0'/0/1 -> 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
```

في الإعداد أعلاه لاحظ التالي:

- العشوائية في المتجه الأول كلها أصفار، فالكلمات 11 مرة `abandon`، وهي الكلمة رقم صفر، ثم `about` التي تحمل في آخر بتاتها مجموع التحقق. وهذا المتجه للاختبار فقط، وأي عبارة من هذا النوع على الشبكة الحقيقية تفرغ في ثوان.
- البذرة والمفتاح الرئيسي `xprv9s21…` يطابقان المتجه الرسمي بالبايت، أي أن كل خطوة في كودنا صحيحة، والمتجهات تستخدم العبارة الإضافية `TREZOR`.
- عبارة Anvil مع المسار `m/44'/60'/0'/0/0` أعطت العنوان `0xf39f…2266` وهو أول حساب اختبار في Anvil، والمسار التالي أعطى الحساب الثاني `0x7099…79c8`، وهما نفس العنوانين اللذين استخدمناهما في المقال الأول. أي أن Anvil لا يخترع حسابات، وإنما يشتقها من عبارة معروفة بنفس الطريقة التي تشتق بها أي محفظة.

## شجرة المفاتيح والمسارات

من المفتاح الرئيسي تشتق المحفظة أبناء، ومن كل ابن أبناء، فتتكون شجرة، وكل مفتاح فيها يعرفه مسار مثل `m/84'/0'/0'/0/5`. والمسارات ليست عشوائية، وإنما متفق عليها حتى تستطيع أي محفظة أن تعيد بناء ما بنته محفظة أخرى من نفس العبارة: [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki?ref=arabroot.io) يحدد المستويات الخمسة: الغرض، ثم نوع العملة، ثم الحساب، ثم فرع الاستلام أو الباقي، ثم رقم العنوان. و[BIP84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki?ref=arabroot.io) يستخدم الغرض 84 لعناوين SegWit الأصلية التي تبدأ بـ `bc1q`، وأرقام العملات في قائمة [SLIP-44](https://github.com/satoshilabs/slips/blob/master/slip-0044.md?ref=arabroot.io)، حيث 0 لـ Bitcoin و1 لكل شبكات الاختبار و60 لـ Ethereum. والصورة التالية تبين المسار:

![مسار من ستة مستويات: m المفتاح الرئيسي، ثم 84 مقوى للغرض BIP84، ثم 1 مقوى للعملة حيث 0 للشبكة الرئيسية و1 للاختبار، ثم 0 مقوى للحساب، ثم 0 عادي للاستلام و1 للباقي، ثم i رقم العنوان، والمستويات المقواة بالأحمر والعادية بالأخضر، وتحته بطاقتان: Ethereum بالمسار m/44/60/0/0/i وعبارة Anvil تعطي 0xf39fd6e5 و0x70997970، ومحفظة الشركة في التجربة حيث العناوين الثلاثة الأولى من الطرف الخاص ومن tpub متطابقة](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-04-tree.webp)

بذرة واحدة، ومسارات متفق عليها، وعدد غير محدود من العناوين

ولاحظ أن نفس العبارة تولد مفاتيح Bitcoin ومفاتيح Ethereum معاً بمسارات مختلفة، وهذا مريح للمستخدم، ولكنه يعني أن تسرب عبارة واحدة يكشف كل الشبكات في وقت واحد. وفي قصتنا، العبارة التي في ملف `.env` قد تكون مستخدمة في شبكات أخرى لا يعرف بها أحد في الشركة.

## تجربة: محفظة جديدة للشركة

الآن نصنع للشركة محفظة جديدة بدلاً من العبارة المكشوفة. والملف التالي يولد عبارة جديدة، ويجرب خطأً في كلمة واحدة، ويبين أثر العبارة الإضافية، ثم يصدر المفتاح الموسع للحساب بصيغتين: خاصة للموقع وعامة للسيرفر، ويتأكد أن العناوين من الطرفين متطابقة، ثم يجرب خطأً سوف نشرحه في قسم مستقل. احفظه باسم `company_wallet.py`:

```python
# A new wallet for the company: 12 words, a typo, a passphrase, a watch-only xpub, and the classic xpub leak.
import secrets
from hd import *

words = entropy_to_words(secrets.token_bytes(16))   # 128 bits of entropy -> 12 words
print("new 12 words      :", " ".join(words[:3]), "... (", len(words), "words )")
typo = words[:]; typo[5] = WORDS[(WORDS.index(typo[5]) + 1) % 2048]
print("valid / with typo :", words_valid(words), "/", words_valid(typo))

seed = words_to_seed(words)
k_a, _ = derive(seed, "m/84'/1'/0'/0/0")
k_b, _ = derive(words_to_seed(words, "finance-only"), "m/84'/1'/0'/0/0")
print("first key, no passphrase  :", hash160(ser_pub(mul(k_a))).hex())
print("first key, with passphrase:", hash160(ser_pub(mul(k_b))).hex())

# account level m/84'/1'/0' (testnet/regtest coin type 1); export tprv for Bitcoin Core and tpub for watch-only
acct_k, acct_c = derive(seed, "m/84'/1'/0'")
parent_k, _ = derive(seed, "m/84'/1'")
fpr = hash160(ser_pub(mul(parent_k)))[:4]
tprv = ser_ext("04358394", 3, fpr, 0x80000000, acct_c, b"\0" + acct_k.to_bytes(32, "big"))
tpub = ser_ext("043587cf", 3, fpr, 0x80000000, acct_c, ser_pub(mul(acct_k)))
open("tprv.txt", "w").write(tprv); open("tpub.txt", "w").write(tpub)
print("tpub (watch-only) :", tpub[:20] + "...")

# receive addresses 0..2 from the PRIVATE side and from the PUBLIC side must match
ext_k, ext_c = child_priv(acct_k, acct_c, 0)
ext_P, ext_cp = child_pub(mul(acct_k), acct_c, 0)
for i in range(3):
    from_priv = hash160(ser_pub(mul(child_priv(ext_k, ext_c, i)[0]))).hex()
    from_pub = hash160(ser_pub(child_pub(ext_P, ext_cp, i)[0])).hex()
    print(f"receive {i}: {from_priv}  same from tpub: {from_priv == from_pub}")

# The leak: tpub of the external branch + ONE non-hardened child private key = the branch private key
child3_k, _ = child_priv(ext_k, ext_c, 3)
I = hmac.new(ext_cp, ser_pub(ext_P) + (3).to_bytes(4, "big"), hashlib.sha512).digest()
recovered = (child3_k - int.from_bytes(I[:32], "big")) % n
print("branch key recovered from xpub + one child key:", recovered == ext_k)
```

```bash
docker run --rm --name lab-wallet-py -e PIP_DISABLE_PIP_VERSION_CHECK=1 -v "$PWD:/lab" -w /lab python:3.13.16-slim \
  bash -c "pip install -q --root-user-action=ignore pycryptodome==3.23.0 && python company_wallet.py"
```

والمخرج سوف يكون كما يلي، والكلمات والقيم تختلف في كل تشغيل:

```text
new 12 words      : tomato fold strike ... ( 12 words )
valid / with typo : True / False
first key, no passphrase  : f533eb6ec1ec64ece4734774f61132cadd081d8b
first key, with passphrase: 7ffbe6d337db93197da3256c1e283125715a74f5
tpub (watch-only) : tpubDD1BCC3NQoLQz8LU...
receive 0: f533eb6ec1ec64ece4734774f61132cadd081d8b  same from tpub: True
receive 1: 156ccae69e501782b5b39811f3708e871bdbe5a4  same from tpub: True
receive 2: a094eedd8154f8e6a4d459e807ac04ca3d0432b6  same from tpub: True
branch key recovered from xpub + one child key: True
```

في الإعداد أعلاه لاحظ التالي:

- الكود يطبع أول ثلاث كلمات فقط، والسبب أن العبارة لا تطبع كاملة في أي سجل أو شاشة، وهذه عادة يجب أن تبنيها في كل أداة داخلية تتعامل مع العبارات.
- عندما غيرنا الكلمة السادسة إلى الكلمة التي بعدها في القائمة رفض مجموع التحقق العبارة. ولكن انتبه: مجموع التحقق 4 بتات فقط، فقرابة واحد من كل 16 خطأ يعطي عبارة صالحة بالصدفة، والمحفظة عندها تفتح محفظة أخرى فارغة دون أي رسالة خطأ، لذلك تحقق دائماً من أول عنوان بعد الاستعادة وقارنه بعنوان تعرفه.
- نفس الكلمات مع العبارة الإضافية `finance-only` أعطت مفتاحاً أول مختلفاً تماماً، أي محفظة أخرى. والعبارة الإضافية تحمي من سرقة الكلمات وحدها، ولكن نسيانها يعني ضياع المحفظة، ولا توجد رسالة تقول لك إنك كتبتها خطأ.
- المفتاح الموسع العام `tpub` للحساب `m/84'/1'/0'` أعطى نفس عناوين الاستلام الثلاثة التي أعطاها الطرف الخاص، وهذا ما يسمح لسيرفر أن يولد عناوين استلام دون أي مفتاح خاص. والبادئة `tpub` تعني شبكة اختبار، وعلى الشبكة الرئيسية تكون `xpub`.

### كيف تعرف المحفظة المستعادة أين توقفت؟ حد الفجوة Gap Limit

عندما تستعيد العبارة على جهاز جديد، تعرف المحفظة كيف تشتق العناوين، ولكنها لا تعرف كم عنواناً استخدمت من قبل، فالشجرة لا نهاية لها. والحل الذي يصفه BIP44 أن تبحث المحفظة في السلسلة عن معاملات على العناوين بالترتيب، فإذا وجدت 20 عنواناً متتالياً لم يستخدمها أحد توقفت وافترضت أنه لا يوجد بعدها شيء، وهذا الرقم يسمى حد الفجوة Gap Limit، وقيمته في المعيار 20\. والمشكلة العملية التي تقع فيها أنظمة استلام المدفوعات أن السيرفر يولد عنواناً جديداً لكل طلب، وكثير من الطلبات لا تدفع، فقد تتراكم عشرات العناوين غير المستخدمة متتالية، ثم تصل دفعة إلى العنوان رقم 45، فلا تراها المحفظة المستعادة لأنها توقفت عند العنوان 20 بعد آخر عنوان مستخدم. ولا تضيع الأموال في هذه الحالة، فالمفتاح ما زال مشتقاً من نفس العبارة، ولكنها لا تظهر حتى تزيد حد الفجوة في إعدادات المحفظة أو تطلب منها البحث أبعد. لذلك إذا كان نظامك يولد عناوين كثيرة، فسجل في قاعدة البيانات أعلى رقم عنوان أعطيته، واجعل إجراء الاستعادة يبحث حتى هذا الرقم على الأقل.

### نتحقق من الشجرة بعقدة Bitcoin Core

الحساب في Python لا يكفي حتى تطابقه أداة مستقلة. نشغل Bitcoin Core في وضع regtest، وننشئ محفظة للمشاهدة فقط بالخيار `disable_private_keys`، ونستورد فيها المفتاح العام الموسع بصيغة الواصفات Descriptors التي يشرحها [توثيق الواصفات](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md?ref=arabroot.io): فرع الاستلام `/0/*` وفرع الباقي `/1/*`، ثم نطلب منها عنواناً:

```bash
docker run -d --name lab-wallet-btc bitcoin/bitcoin:31.1 bitcoind -regtest -fallbackfee=0.0001 -printtoconsole=0
alias btc='docker exec --user bitcoin lab-wallet-btc bitcoin-cli -regtest'
TPUB=$(cat tpub.txt)
D0=$(btc getdescriptorinfo "wpkh($TPUB/0/*)" | grep '"descriptor"' | cut -d'"' -f4); echo $D0
D1=$(btc getdescriptorinfo "wpkh($TPUB/1/*)" | grep '"descriptor"' | cut -d'"' -f4)
btc -named createwallet wallet_name=company-watch disable_private_keys=true blank=true | head -2
btc -rpcwallet=company-watch importdescriptors "[{\"desc\":\"$D0\",\"timestamp\":\"now\",\"active\":true,\"internal\":false},{\"desc\":\"$D1\",\"timestamp\":\"now\",\"active\":true,\"internal\":true}]" | grep success
R=$(btc -rpcwallet=company-watch getnewaddress); echo $R
btc validateaddress $R | grep witness_program
```

والمخرج سوف يكون كما يلي:

```text
wpkh(tpubDD1BCC3NQoLQz8LUKCGHCCokmUJmYSRM6Q1kEP2Vyb5RLPhAPLGuSSJrFhMYYAZ5wrxS2KxLUMmm9RX8rSE465SWvSuDn6dBwdtD3ZjBkY3/0/*)#jmwpg393
{
  "name": "company-watch"
    "success": true,
    "success": true,
bcrt1q75e7kmkpa3jweernga60vyfjetwss8vt6ygk8j
  "witness_program": "f533eb6ec1ec64ece4734774f61132cadd081d8b"
```

أول عنوان أعطته محفظة المشاهدة يحمل نفس الهاش `f533eb6e…` الذي حسبه كودنا للعنوان `receive 0`، أي أن Bitcoin Core وكودنا يتفقان على كل خطوة من البذرة إلى العنوان. ولاحظ أن محفظة السيرفر الآن تعرف كل عناوين الشركة، ولا تملك أي مفتاح يستطيع أن يصرف منها.

## محفظة مشاهدة على السيرفر وتوقيع منفصل PSBT

في قصتنا، سبب وضع العبارة في ملف `.env` كان أن أداة النشر تحتاج أن توقع معاملات تلقائياً. وهذا هو الخلط الذي يجب أن تفصله: معرفة العناوين والأرصدة وبناء المعاملات شيء، والتوقيع شيء آخر. والطريقة المعيارية لفصلهما في Bitcoin هي المعاملة الموقعة جزئياً Partially Signed Bitcoin Transaction، واختصاراً PSBT، الموصوفة في [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki?ref=arabroot.io) ويشرحها [توثيق PSBT في Bitcoin Core](https://github.com/bitcoin/bitcoin/blob/master/doc/psbt.md?ref=arabroot.io): محفظة المشاهدة تبني المعاملة وتحسب الرسوم والباقي وتكتب في الملف كل ما يحتاجه الموقع، ثم ينتقل الملف إلى جهاز يحمل المفتاح، فيعرض المبلغ والعنوان ويوقع، ثم يرجع الملف ويرسل.

نمول عنوان الشركة من محفظة معدن في نفس الشبكة التجريبية، ثم نحاول أن نصرف من محفظة المشاهدة مباشرة، ثم نبني PSBT بدلاً من ذلك:

```bash
btc createwallet miner
M=$(btc -rpcwallet=miner getnewaddress)
btc generatetoaddress 101 $M > /dev/null
btc -rpcwallet=miner sendtoaddress $R 2 > /dev/null
btc generatetoaddress 1 $M > /dev/null
btc -rpcwallet=company-watch getbalances | sed -n 1,8p
btc -rpcwallet=company-watch sendtoaddress $M 0.5
PSBT=$(btc -rpcwallet=company-watch walletcreatefundedpsbt "[]" "[{\"$M\":0.5}]" | grep '"psbt"' | cut -d'"' -f4)
echo ${PSBT:0:40}...
btc decodepsbt $PSBT | grep -E '"(fee)"'
```

والمخرج سوف يكون كما يلي، بعد سطر اسم محفظة المعدن:

```text
{
  "mine": {
    "trusted": 2.00000000,
    "untrusted_pending": 0.00000000,
    "immature": 0.00000000
  },
  "lastprocessedblock": {
    "hash": "087c6cc166911b819ea4223bea19134157b8936ba5b6d6caa243414eabdcce4c",
error code: -4
error message:
Error: Private keys are disabled for this wallet
cHNidP8BAHECAAAAAV4U851MqoDsI7GFVH4PDV2s...
  "fee": 0.00001410
```

محفظة المشاهدة ترى الرصيد 2، وترفض أن ترسل لأنها لا تملك مفاتيح، ولكنها بنت معاملة كاملة بالرسوم والباقي في ملف PSBT يبدأ بـ `cHNidP8`، وهو ترميز Base64 للبايتات `psbt` في بداية كل ملف من هذا النوع. والآن نمثل الموقع بمحفظة ثانية نستورد فيها المفتاح الموسع الخاص. وفي الإنتاج يكون الموقع جهازاً منفصلاً أو محفظة عتاد، ولا يمر المفتاح الخاص أبداً في سطر أوامر كما نفعل هنا، لأن الأوامر تحفظ في سجل ال Shell:

```bash
TPRV=$(cat tprv.txt)
E0=$(btc getdescriptorinfo "wpkh($TPRV/0/*)" | grep '"checksum"' | cut -d'"' -f4)
E1=$(btc getdescriptorinfo "wpkh($TPRV/1/*)" | grep '"checksum"' | cut -d'"' -f4)
btc -named createwallet wallet_name=company-signer blank=true | head -2
btc -rpcwallet=company-signer importdescriptors "[{\"desc\":\"wpkh($TPRV/0/*)#$E0\",\"timestamp\":\"now\",\"active\":true,\"internal\":false},{\"desc\":\"wpkh($TPRV/1/*)#$E1\",\"timestamp\":\"now\",\"active\":true,\"internal\":true}]" | grep success
SIG=$(btc -rpcwallet=company-signer walletprocesspsbt $PSBT | grep -E '"(complete)"|"psbt"'); echo "$SIG" | grep complete
SP=$(echo "$SIG" | grep '"psbt"' | cut -d'"' -f4)
FIN=$(btc finalizepsbt $SP | grep '"hex"' | cut -d'"' -f4)
T=$(btc sendrawtransaction $FIN); echo "txid $T"
btc generatetoaddress 1 $(btc -rpcwallet=miner getnewaddress) > /dev/null
btc -rpcwallet=company-watch getbalances | sed -n 3,3p
btc -rpcwallet=company-watch gettransaction $T | grep -E '"(amount|fee|confirmations)"' | head -3
```

والمخرج سوف يكون كما يلي:

```text
{
  "name": "company-signer"
    "success": true,
    "success": true,
  "complete": true,
txid 6e057552475c0272851249e158f3f1f22a2713c087be1e0d15502cbe76867843
    "trusted": 1.49998590,
  "amount": -0.50000000,
  "fee": -0.00001410,
  "confirmations": 1,
```

في الإعداد أعلاه لاحظ التالي:

- الموقع أرجع `complete: true`، أي أن كل مدخل في المعاملة أصبح عليه توقيع صحيح، ولم يحتج الموقع أن يعرف شيئاً عن الشبكة أو الأرصدة، فكل ما يلزمه موجود في ملف PSBT.
- محفظة المشاهدة هي التي أنهت الملف وأرسلته، ورصيدها أصبح 1.49998590، أي 2 ناقص 0.5 ناقص الرسوم 0.0000141 التي حسبتها هي عند البناء.
- الرسوم في `decodepsbt` قبل التوقيع هي نفسها في المعاملة النهائية، وهذا ما يجعل الموقع يستطيع أن يعرض للمستخدم المبلغ والرسوم الحقيقية قبل أن يوقع.

والصورة التالية تجمع المراحل:

![أربع مراحل: محفظة المشاهدة على السيرفر مستوردة من tpub ترى رصيد 2 وترفض الإرسال برسالة Private keys are disabled، ثم معاملة غير موقعة PSBT من walletcreatefundedpsbt برسوم 0.00001410، ثم الموقع الذي يحمل المفتاح الخاص يوقع بالأمر walletprocesspsbt فتصبح complete true، ثم الإرسال بمعرف 6e057552 ورصيد 1.49998590](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-05-psbt.webp)

السيرفر يعرف ويبني ولا يوقع، والمفتاح الخاص يبقى في مكان آخر

وفي Ethereum نفس الفكرة موجودة بأشكال أخرى، فأدوات مثل `cast` تستطيع أن تبني المعاملة وتطبعها دون توقيع، وأدوات النشر تستطيع أن توقع عبر محفظة عتاد متصلة بدلاً من مفتاح في ملف. والقاعدة لقصتنا: أداة النشر الآلية لا تحمل مفتاح الشبكة الرئيسية، وإنما مفتاحاً منفصلاً برصيد صغير يكفي لمهمة واحدة، أو تجهز المعاملة ويوقعها إنسان على جهاز منفصل.

## نجرب العطل: مفتاح عام موسع ومفتاح ابن واحد

في المخرج السابق كان السطر الأخير `branch key recovered from xpub + one child key: True`، وهذا هو الخطأ الذي وعدنا به. والقصة أن المفتاح العام الموسع ليس مفتاحاً عاماً عادياً، فهو يحمل معه رمز السلسلة Chain Code، وبه تحسب أي جهة الرقم الذي أضيف إلى مفتاح الأب لتنتج كل ابن عادي. فإذا عرف شخص المفتاح العام الموسع لفرع، وعرف بأي طريقة المفتاح الخاص لابن واحد فيه، يستطيع أن يطرح الرقم من مفتاح الابن فيخرج المفتاح الخاص للفرع، ومنه كل عناوين الاستلام الماضية والقادمة. والكود فعل ذلك في سطرين، وهذا التحذير مكتوب في قسم الأمان في BIP32 نفسه.

![ثلاث خطوات: المهاجم يعرف tpub للفرع ومفتاحاً خاصاً لعنوان واحد تسرب، ثم يحسب I من HMAC برمز السلسلة والمفتاح العام والرقم 3 ويطرح النصف الأيسر من مفتاح الابن، ثم يحصل على المفتاح الخاص للفرع كاملاً، وتحتها بطاقتان: المستويات العليا مقواة لأن الاشتقاق المقوى يستخدم المفتاح الخاص للأب فلا يمكن عكسه، والقاعدة العملية أن تعامل xpub كبيانات حساسة ولا تصدر مفتاحاً خاصاً لعنوان واحد](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-07-xpub-leak.webp)

xpub ومفتاح ابن عادي واحد يكفيان لحساب مفتاح الفرع كله

ولهذا السبب المستويات الثلاثة الأولى في المسار مقواة: الاشتقاق المقوى يدخل فيه المفتاح الخاص للأب نفسه، فلا يمكن حسابه من أي مفتاح عام، وبالتالي إذا تسرب حساب كامل بسبب هذا الخطأ تبقى الحسابات الأخرى في نفس العبارة معزولة. والدروس العملية لشركتنا اثنان: الأول أن المفتاح العام الموسع ليس سراً يحمي الأموال، ولكنه بيانات حساسة تكشف كل معاملات الحساب لمن يملكها، فلا تعطه لأي خدمة إلا إذا قبلت أن ترى كل شيء. والثاني ألا تصدر أبداً مفتاحاً خاصاً لعنوان واحد من محفظة مشتقة لتضعه في أداة أخرى، وإذا احتاجت أداة مفتاحاً مستقلاً فولد لها عبارة منفصلة.

## المفاتيح في Ethereum: ما الذي توقع عليه بالضبط؟

المفتاح في Ethereum يوقع نوعين من الأشياء، والخلط بينهما من أخطر الأخطاء. الأول المعاملات: تحويل قيمة أو استدعاء عقد ذكي، وفيها عداد Nonce للحساب ورقم الشبكة Chain ID كما رأينا في المقال الأول، فلا تصلح المعاملة لشبكة أخرى ولا يمكن إعادة إرسالها. والثاني الرسائل: نص أو بيانات منظمة يوقعها المستخدم ليثبت أنه صاحب العنوان، مثل تسجيل الدخول إلى تطبيق، وقد رأينا توقيع رسالة بالأمر `cast wallet sign` في مقال التشفير. والرسائل لا تدخل السلسلة، ولكن بعض العقود الذكية تقبل رسالة موقعة كإذن، فالتوقيع على رسالة لم تفهمها قد يعطي صلاحية حقيقية. ولهذا جاء معيار [EIP-712](https://eips.ethereum.org/EIPS/eip-712?ref=arabroot.io) للبيانات المنظمة Typed Data، حتى تعرض المحفظة للمستخدم حقولاً مفهومة بدلاً من سلسلة Hex لا معنى لها.

والنوع الأخطر من المعاملات هو الموافقة Approve على عقد رمز Token: معاملة تعطي عقداً آخر صلاحية أن يسحب من رموزك حتى مبلغ معين، أو دون حد في كثير من التطبيقات لتوفير الرسوم. وهذه الموافقة تبقى سارية حتى تلغيها بمعاملة أخرى، حتى لو نسيت التطبيق الذي أعطيته إياها، فإذا اخترق ذلك العقد لاحقاً أو كان خبيثاً من البداية سحب ما وافقت عليه. لذلك في سياسة الشركة: كل موافقة بحد يساوي المبلغ المطلوب فقط، ومراجعة دورية للموافقات القائمة على عناوين الشركة وإلغاء ما لم يعد مستخدماً. وسوف نشرح الرموز والموافقات بالكود في مقال معيار ERC-20.

## محافظ العتاد Hardware Wallets: ماذا تحمي وماذا لا تحمي؟

محفظة العتاد جهاز صغير يولد العبارة بداخله ويحفظ المفاتيح، ولا يسمح لها أن تخرج منه، والكمبيوتر المتصل به يرسل له المعاملة أو ملف PSBT، فيعرضها الجهاز على شاشته، ويضغط المستخدم زراً للموافقة، فيوقع الجهاز ويرجع التوقيع فقط. والصورة التالية لجهازين من هذا النوع بجانب عملة Casascius المعدنية التي سنعود إليها في قسم النسخ الاحتياطي:

![جهازا محفظة عتاد صغيران بشاشة وزرين على قماش أحمر، أحدهما أسود والآخر فضي، وبجانبهما عملة معدنية فضية عليها شعار Bitcoin](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-03-hardware-wallet.webp)

محفظتا عتاد بجانب عملة Casascius معدنية. الصورة: Gage Skidmore، رخصة CC BY-SA 3.0، المصدر: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Two%5FTrezor%5FOne%5Fhardware%5Fwallets%5Fand%5Fa%5F5BTC%5FCasascius%5Fphysical%5Fcoin%5Fby%5FGage%5FSkidmore.jpg?ref=arabroot.io) (تم تصغيرها وتحويلها إلى WebP).

والذي تحميه هذه الأجهزة واضح: برنامج خبيث على الكمبيوتر لا يستطيع أن يقرأ المفتاح، ولا يستطيع أن يوقع دون ضغطة الزر. ولكن هناك ثلاثة أشياء لا تحميها:

- **ما توافق عليه**: إذا عرض الجهاز عنواناً غير الذي تقصده ووافقت عليه دون أن تقرأه، فالجهاز وقع بأمانة على ما طلبته. وفي Ethereum الخطر أكبر، لأن كثيراً من المعاملات استدعاءات لعقود لا يفهمها الإنسان من شاشة صغيرة، ومن أشهر الحيل طلب موافقة Approve تعطي عقداً صلاحية سحب رموزك كما حذرت صفحة الأمان في ethereum.org التي ذكرناها في المقال الأول. والقاعدة: إذا لم تفهم ما تعرضه الشاشة فلا توافق.
- **العبارة المكتوبة خارجه**: الجهاز يحمي المفتاح داخله، ولكن نسخة العبارة الاحتياطية على الورق خارج الجهاز، ومن يصل إليها لا يحتاج الجهاز أصلاً.
- **بيانات العميل**: شراء الجهاز يترك بيانات عند البائع وشركات الشحن، وهذه البيانات تصبح أداة للتصيد إذا تسربت، كما في الحادثة التالية.

وهناك نقطة رابعة تخص طريقة الشراء نفسها: اشتر الجهاز من المصنع أو من موزع يذكره المصنع على موقعه، وحدث برنامجه من المصدر الرسمي فقط، ولا تقبل أبداً جهازاً يصلك ومعه بطاقة عليها كلمات مكتوبة مسبقاً أو عبارة «تم إعداد المحفظة لك»، والسبب أن الجهاز الصحيح يولد العبارة بنفسه أمامك لأول مرة، وأي عبارة سبق أن رآها شخص آخر قبل أن تصل إليك فهي عبارة يملكها ذلك الشخص أيضاً.

📌

في يوليو 2020 أعلنت شركة Ledger في [بيان رسمي](https://www.ledger.com/addressing-the-july-2020-e-commerce-and-marketing-data-breach?ref=arabroot.io) أن طرفاً غير مصرح له وصل في 25 يونيو 2020 إلى قاعدة بيانات التجارة الإلكترونية والتسويق، وأن البيانات المكشوفة كانت في أغلبها قرابة مليون عنوان بريد إلكتروني لعملائها، ومعها لمجموعة من 9,500 عميل تفاصيل مثل الأسماء والعناوين البريدية وأرقام الهواتف والمنتجات المطلوبة. وذكرت أن معلومات الدفع والأموال الرقمية في أمان وأن الأجهزة نفسها لم تتأثر، وحذرت عملاءها من رسائل التصيد، وقالت إنها لن تطلب أبداً عبارة الاسترداد المكونة من 24 كلمة، وإن أي رسالة تطلبها تصيد. والدرس أن الجهاز قد يكون آمناً تماماً، والهجوم يأتي على الإنسان الذي يملكه، برسالة تبدو رسمية تطلب العبارة.

## النسخ الاحتياطي: أين تحفظ العبارة؟

العبارة هي النسخة الاحتياطية الوحيدة للمحفظة، وأي خطأ في حفظها إما يضيعها أو يكشفها. والفكرة القديمة لحفظ مفتاح في شيء مادي تبينها عملة Casascius في الصورة التالية: عملة معدنية فيها مفتاح خاص حقيقي مخفي تحت ملصق ثلاثي الأبعاد Hologram يظهر عليه أثر إذا نزعه أحد، أي أن من يحمل العملة يحمل المال نفسه، كما يحمل ورقة نقدية:

![عملة معدنية ذهبية اللون عليها ملصق ثلاثي الأبعاد ملون عليه شعار Bitcoin وكلمة CASASCIUS وجزء من عنوان مطبوع، موضوعة على سطح جهاز رمادي](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-06-casascius-coin.webp)

عملة Casascius، والمفتاح الخاص مطبوع تحت الملصق الثلاثي الأبعاد. الصورة: Steve Jurvetson، رخصة CC BY 2.0، المصدر: [Wikimedia Commons](https://commons.wikimedia.org/wiki/File:Casascius%5Fcoin.jpg?ref=arabroot.io) (تم تصغيرها وتحويلها إلى WebP).

والمبدأ نفسه ينطبق على العبارة المكتوبة: من يراها يملك المحفظة. لذلك القواعد التالية ليست اختيارية:

- **على شيء مادي فقط**: ورقة أو لوح معدني يتحمل الحريق والماء، ولا تكتبها أبداً في ملف أو بريد أو تطبيق ملاحظات أو صورة، والسبب أن هذه كلها تتزامن تلقائياً مع خدمات سحابية لا تتحكم فيها، وصورة الهاتف في قصتنا موجودة على الأغلب في نسخة سحابية لا يعرف أحد من يصل إليها.
- **في مكانين على الأقل**: نسخة واحدة تضيع في حريق أو انتقال مكتب، ونسختان في مكانين مختلفين يحميان من الضياع، ولكنهما يضاعفان فرصة الكشف، لذلك يحتاج كل مكان حماية مادية.
- **مع اختبار استعادة**: بعد الكتابة استعد المحفظة على جهاز آخر وقارن أول عنوان، كما فعلنا في التجربة، فهذا ما يكشف كلمة كتبت خطأ قبل أن تحتاجها فعلاً.
- **العبارة الإضافية في مكان آخر**: إذا استخدمت Passphrase فلا تكتبها مع الكلمات، وإلا فقدت فائدتها، واكتبها في مكان آخر حتى لا تضيع.
- **الواصف مع النسخة في المحافظ المتعددة**: في محفظة التوقيع المتعدد لا تكفي العبارات وحدها لإعادة بناء العناوين، وإنما تحتاج أيضاً الواصف Descriptor الذي يجمع المفاتيح العامة لكل الأطراف وعدد التوقيعات المطلوب، فاحفظه مع كل نسخة.

## التوقيع المتعدد Multisig ونماذج الحفظ للشركات

المشكلة الأساسية في قصتنا ليست تقنية: الشركة تريد ألا يستطيع شخص واحد أن يصرف وحده، وألا تضيع الأموال إذا غادر شخص واحد أو أضاع شيئاً. والأداة لذلك في Bitcoin هي التوقيع المتعدد Multisig: قفل يتطلب مثلاً توقيعين من ثلاثة مفاتيح، كل مفتاح عند شخص مختلف في جهاز مختلف ومكان مختلف، فلا يكفي مفتاح مسروق واحد للصرف، ولا يمنع مفتاح ضائع واحد الصرف. وفي Ethereum يتم ذلك بعقد ذكي يحمل الأموال ويطلب موافقة عدة مالكين، وهنا يصبح كود العقد نفسه جزءاً من الأمان، وسوف نعود إلى مخاطر كود العقود في مقالاتها.

والصورة التالية تلخص النماذج الثلاثة وثمن كل نموذج:

![جدول من ثلاثة نماذج: مفتاح واحد يصرف به من يملك العبارة أو الجهاز وخطره أن شخصاً واحداً يضيعها أو يسرقها أو يغادر ويناسب مبالغ التشغيل الصغيرة، وتوقيع متعدد 2 من 3 يصرف به أي اثنين وخطره تعقيد الإجراءات وضرورة حفظ الواصف ويناسب خزينة الشركة، وطرف حافظ يصرف بحسب العقد وخطره إفلاسه أو اختراقه ويناسب من لا يستطيع إدارة المفاتيح، وتحته بطاقة سياسة مكتوبة لكل النماذج وبطاقة ما لا تفعله أبداً](https://arabroot.io/content/images/2026/10/crypto-wallets-keys-08-custody.webp)

كل نموذج يختار خطراً يقبله: السرقة أو الضياع أو الاعتماد على طرف آخر

ولاحظ أن الطرف الحافظ في الجدول ليس خطأً في ذاته، فكثير من المؤسسات لا تملك القدرة على إدارة مفاتيح بأمان، ولكنه يعيدك إلى سؤال الثقة الذي بدأنا به السلسلة كلها: هل الطرف مرخص ومراقب في البلد الذي يعمل فيه، وهل تفصل أموال العملاء عن أمواله، وماذا يحدث لأموالك إذا أفلس؟ وسوف نناقش هذه الأسئلة بالتفصيل في مقال الحفظ لدى المنصة أم بنفسك. وفي كل الأحوال، أي قرار لشركة تحتفظ بأصول رقمية يحتاج مراجعة قانونية ومحاسبية في البلد الذي تعمل فيه قبل القرار التقني، وفي المملكة ذكرنا في مقال تاريخ العملات الرقمية بيان اللجنة الدائمة سنة 2018 بأن العملات الافتراضية غير معتمدة داخل المملكة.

## الاستمرارية: ماذا يحدث إذا غاب صاحب المفتاح؟

سؤال يتجنبه كثير من الفرق حتى يقع: ماذا يحدث إذا غاب الشخص الوحيد الذي يعرف أين النسخة الاحتياطية، أو توفي، أو رفض التعاون بعد خلاف؟ في البنك هناك إجراءات قانونية تنقل الحساب، أما في الحفظ الذاتي فالشبكة لا تعرف شيئاً عن العقود والمحاكم، والمفتاح الضائع يعني أموالاً لا يصل إليها أحد أبداً. لذلك يجب أن تصمم الاستمرارية من البداية كما تصمم استمرارية الأعمال Business Continuity لأي نظام حرج: من يحمل كل مفتاح، ومن يعرف مكان كل نسخة، وما الإجراء المكتوب لاستعادة الأموال إذا غاب شخص، ومن يراجع هذا الإجراء دورياً.

والتوقيع المتعدد يحل جزءاً كبيراً من هذه المشكلة، فغياب شخص واحد في محفظة 2 من 3 لا يمنع الصرف، ويسمح للشخصين الباقيين بنقل الأموال إلى محفظة جديدة بمفتاح ثالث جديد. وفي Bitcoin يمكن أن يضيف القفل شرطاً زمنياً بالأقفال التي رأيناها في مقال كيف يعمل Bitcoin، مثل مفتاح احتياطي عند جهة قانونية لا يصلح للصرف إلا بعد مدة طويلة من آخر حركة، وهذا تصميم متقدم يحتاج اختباراً دقيقاً على شبكة تجريبية قبل أن تضع فيه مالاً حقيقياً. والأهم من الأداة أن يكون الإجراء مكتوباً ومعروفاً لأكثر من شخص، فالإجراء الذي في رأس شخص واحد ليس إجراءً.

## سياسة مفاتيح مكتوبة لشركة صغيرة

بعد كل ما سبق، هذا ما خرجت به الشركة في قصتنا، وهو نموذج تستطيع أن تبدأ منه وتعدله:

| البند              | القرار                                                                            | والسبب                                         |
| ------------------ | --------------------------------------------------------------------------------- | ---------------------------------------------- |
| العبارة المكشوفة   | تعتبر مسروقة: ينقل الرصيد فوراً إلى محفظة جديدة، ولا يستخدم أي عنوان منها بعد ذلك | لا يمكن إثبات أن أحداً لم ينسخها               |
| مفتاح النشر الآلي  | عبارة منفصلة لكل بيئة، برصيد صغير يكفي لمهمة، ويعاد شحنه يدوياً                   | تسرب ملف لا يكشف إلا مبلغاً صغيراً             |
| الخزينة            | توقيع متعدد 2 من 3، والمفاتيح على أجهزة عتاد عند ثلاثة أشخاص في مكانين مختلفين    | لا يصرف شخص وحده، ولا يمنع غياب شخص واحد الصرف |
| السيرفر            | محفظة مشاهدة بالمفتاح العام الموسع فقط، تبني PSBT ولا توقع                        | اختراق السيرفر لا يعطي صلاحية صرف              |
| النسخ الاحتياطي    | على معدن، في مكانين، مع الواصف، واختبار استعادة كل ستة أشهر                       | ضياع نسخة لا يضيع الأموال، والخطأ يكتشف مبكراً |
| مغادرة موظف        | إذا كان يحمل مفتاحاً في التوقيع المتعدد ينقل الرصيد إلى محفظة بمفتاح جديد         | الموظف السابق لا يبقى نصف صلاحية               |
| الرسائل والاتصالات | لا أحد يطلب العبارة لأي سبب، وأي طلب يعامل كتصيد ويبلغ عنه                        | هذا ما تقوله كل شركات المحافظ نفسها            |

## أخطاء شائعة

- **العبارة في مستودع Git**: حذف الملف لا يكفي، لأن السجل Git History يحتفظ به، وأي نسخة من المستودع عند أي مطور تحمله، لذلك تعامل مع أي عبارة دخلت مستودعاً كأنها نشرت.
- **مفاتيح الاختبار على الشبكة الحقيقية**: عبارة Anvil ومتجهات BIP39 معروفة للجميع، وهناك من يراقب عناوينها ليسحب أي مبلغ يصلها فوراً.
- **توليد العبارة على موقع ويب**: أي صفحة تولد لك عبارة يستطيع صاحبها أن يحتفظ بنسخة منها، والعبارة تولد على الجهاز الذي سيحفظها فقط.
- **الوثوق بعبارة صالحة شكلاً**: مجموع التحقق يمرر قرابة واحد من كل 16 خطأ، فتحقق دائماً من أول عنوان بعد الاستعادة.
- **إعطاء xpub لكل خدمة**: هذا يكشف كل معاملاتك لها، ومع مفتاح ابن واحد يكشف الفرع كله كما رأينا.

## إزالة التجربة

```bash
docker rm -f -v lab-wallet-btc
docker image rm bitcoin/bitcoin:31.1
rm -f tprv.txt tpub.txt
```

## تدريب عملي

1. غير `company_wallet.py` ليولد 24 كلمة بدلاً من 12، واحسب كم بتاً من مجموع التحقق في آخر كلمة.
2. اشتق بنفس العبارة عنوان Ethereum بالمسار `m/44'/60'/0'/0/0`، ثم تأكد منه بالأمر `cast wallet address --mnemonic` في Foundry.
3. كرر تجربة PSBT ولكن بمحفظة توقيع متعدد 2 من 3 بالواصف `wsh(multi(2,…))`، ووقع بمحفظتين مختلفتين على نفس الملف.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- المحفظة تحفظ مفاتيح وليس عملات، ومن يملك المفتاح يملك القرار، سواءً كان أنت أو منصة تحفظ عنك.
- BIP39 يحول العشوائية إلى كلمات مع مجموع تحقق، ثم إلى بذرة بدالة بطيئة، وBIP32 يشتق من البذرة شجرة مفاتيح بمسارات متفق عليها، وتحققنا من كودنا بالمتجه الرسمي وبعبارة Anvil وبعقدة Bitcoin Core.
- العبارة الإضافية تعطي محفظة مختلفة تماماً، ومجموع التحقق لا يكشف كل الأخطاء، فتحقق من أول عنوان بعد كل استعادة.
- محفظة المشاهدة على السيرفر تبني المعاملات من المفتاح العام الموسع، والتوقيع يتم على جهاز منفصل بصيغة PSBT.
- المفتاح العام الموسع مع مفتاح ابن عادي واحد يكشف الفرع كله، ولهذا المستويات العليا مقواة، وxpub بيانات حساسة.
- محافظ العتاد تحمي المفتاح من الكمبيوتر، ولا تحميك مما توافق عليه ولا من التصيد، وحادثة Ledger سنة 2020 مثال على الثاني.
- الشركة تحتاج سياسة مكتوبة: توقيع متعدد للخزينة، ومفاتيح منفصلة صغيرة للآلة، ونسخ احتياطية مادية مختبرة، وإجراء عند مغادرة أي شخص.

[![](https://arabroot.io/content/images/2026/10/blockchain-basics-cover-cover.webp)دليلكيف يعمل البلوكتشين Blockchain ومتى لا تحتاجهما المشكلة التي يحلها البلوكتشين أصلاً وكيف يعمل من الداخل، ومتى تكون قاعدة البيانات العادية هي الحل الأصح.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-%D9%88%D9%85%D8%AA%D9%89-%D9%84%D8%A7-%D8%AA%D8%AD%D8%AA%D8%A7%D8%AC%D9%87/)[![](https://arabroot.io/content/images/2026/10/blockchain-cryptography-cover-cover.webp)دليلالتشفير في البلوكتشين: الهاش والمنحنيات والتوقيعاتكيف تصنع المفاتيح والعناوين والتوقيعات في Bitcoin وEthereum، ولماذا يكشف رقم مكرر واحد في التوقيع المفتاح الخاص.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%81%D9%8A-%D8%A7%D9%84%D8%A8%D9%84%D9%88%D9%83%D8%AA%D8%B4%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%AD%D9%86%D9%8A%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D9%82%D9%8A%D8%B9%D8%A7%D8%AA/)[![](https://arabroot.io/content/images/2026/10/bitcoin-how-it-works-cover-cover.webp)دليلكيف يعمل Bitcoin؟ المعاملة والمخرجات غير المنفقة UTXOنبني معاملة Bitcoin بأيدينا ونتتبعها حتى الكتلة، لنفهم لماذا لا يوجد رصيد، وأين تذهب الرسوم، ومتى تصبح الدفعة دفعة فعلاً.](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D9%8A%D8%B9%D9%85%D9%84-bitcoin-%D8%A7%D9%84%D9%85%D8%B9%D8%A7%D9%85%D9%84%D8%A9-%D9%88%D8%A7%D9%84%D9%85%D8%AE%D8%B1%D8%AC%D8%A7%D8%AA-%D8%BA%D9%8A%D8%B1-%D8%A7%D9%84%D9%85%D9%86%D9%81%D9%82%D8%A9-utxo-%D9%88%D8%A7%D9%84%D9%83%D8%AA%D9%84%D8%A9/)[![](https://arabroot.io/content/images/2026/10/crypto-for-devs-cover-cover.webp)دليلالتشفير للمطورين: ماذا تستخدم ولماذا لا تخترع خوارزميتكأي هاش تستخدم لكلمات المرور، ومتى تحتاج التشفير المتماثل أو غير المتماثل، وأين تحفظ المفاتيح بأمان في تطبيقك.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD/)[![](https://arabroot.io/content/images/2026/10/encodings-cover-cover.webp)دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/)[![](https://arabroot.io/content/images/2026/10/bitcoin-node-cover-cover.webp)دليلتشغيل عقدة Bitcoin كاملة على سيرفرك مع Bitcoin Coreلماذا تشغل عقدة Bitcoin كاملة، وكيف تثبتها بـ Docker Compose وتؤمن واجهة RPC، وماذا يحدث في المزامنة الأولى والتقليم.](https://arabroot.io/articles/%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-%D8%B9%D9%82%D8%AF%D8%A9-bitcoin-%D9%83%D8%A7%D9%85%D9%84%D8%A9-%D8%B9%D9%84%D9%89-%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D9%83-%D9%85%D8%B9-bitcoin-core/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، واختبار التجارب على Docker Engine 29.8.0 مع Python 3.13.16 ومكتبة pycryptodome 3.23.0، وBitcoin Core v31.1.0 في وضع regtest.