اقترحت وسم "23s ago" لبيانات on-chain. أراني Claude أنّها كذبة منتج.
smarts.md موقع وثائق حيّة (live docs) للعقود الذكية. كل صفحة عقد تعرض مجموعة من القيم المقروءة مباشرةً من السلسلة — سيولة pool في Uniswap، إجمالي العرض لـ ERC-20، مالك عقد مخصّص. وقد تكون هذه القيم قديمة بحدّ أقصى 60 ثانية (Solid Cache TTL = 60s).
لكن الصفحة لم تكن تتضمّن أي إشارة إلى "متى تمّت قراءة هذه القيمة؟". بدت كأنها Mintlify — لا فرق بينها وبين وثيقة ثابتة.
أثناء مراجعتي مع Claude لاستراتيجية تحديث البيانات، أحصيت ست ثغرات. الثغرة الرابعة كانت في سطر واحد:
البيانات الحيّة بلا مؤشّر طزاجة — أضِف وسماً "as of block X / Y seconds ago".
كتبتُ المهمّة وذهبت لتناول الغداء. عند العودة، توقّفت ساعةً عند هذا السطر. الاتّجاه الذي أطلقته في النهاية كان مختلفاً تماماً عن حدسي الأوّل — وساعدني Claude في إدراك أنّ الخطّة الأصليّة في حدّ ذاتها كذبة منتج.
أوّل ما خطر لي:
liquidity: 1,234,567 USDC ← 23s ago
totalSupply: 27.3B USDC ← 12s ago
بسيط، مباشر، يفهمه المستخدم فوراً.
بعد الغداء نظرت ثانيةً وبدأت أطرح بضعة أسئلة على Claude:
هذا "23s" يطرح ماذا من ماذا بالضبط؟
أوضحه: now - cache.fetched_at. أي الزمن منذ أن كتبنا القيمة في الذاكرة المؤقّتة.
إذاً، كم نتأخّر فعلياً عن رأس السلسلة؟
فكّر Claude: Multicall لديك يقرأ كل دوال view دفعةً واحدة، يكتبها في الذاكرة المؤقّتة، وتظل الذاكرة صالحة لـ 60s. لكن الـ block الذي قرأنا عنده لحظة الكتابة هو نفسه ينتمي إلى الماضي — حدّ Ethereum الأدنى لزمن الكتلة 0–12s.
السلسلة السببيّة الفعليّة:
رأس السلسلة ينتج كتلة جديدة
└→ RPC يقرأ عند الكتلة N (تأخّر 0–12s عن الرأس)
└→ نكتب في Solid Cache
└→ التصيير على شاشة المستخدم (الذاكرة حتى 60s)
إذاً وسم "23s ago" يقابل أي مقطع؟
المقطع الأخير فقط: now - fetched_at، الزمن من كتابة الذاكرة إلى الآن.
التأخّر على جهة السلسلة لا ينعكس إطلاقاً.
مثال محسوس: يرى المستخدم "fetched 5s ago"، لكن تلك الكتلة عمرها أصلاً 12s، وقد ينتج رأس السلسلة كتلتين إضافيّتين منذ ذلك الحين — التأخّر الفعلي عن السلسلة ≈ 24s، والوسم يقول 5s.
في تلك اللحظة أدركت: ضغط "الطزاجة" في رقم واحد خاطئ فيزيائيّاً.
ساعدني Claude على كتابة ذلك بوضوح — هناك ثلاثة أبعاد مستقلّة:
| البعد | ما يقيسه | الحدّ الفيزيائي الأدنى |
|---|---|---|
| طزاجة الكتلة | رأس السلسلة → الكتلة التي قرأناها | زمن كتلة Ethereum 0–12s |
| طزاجة القراءة | قراءة RPC → كتابة الذاكرة | 0 (متزامن) |
| طزاجة العرض | كتابة الذاكرة → شاشة المستخدم | 0–60s (TTL الذاكرة) |
جمع هذه الثلاثة في رقم واحد "freshness: 35s" لا وجود له فيزيائيّاً. سلسلة chain → cache → screen ليست متزامنة: تتجمّد الذاكرة لحظة الكتابة، فيما تواصل السلسلة إنتاج الكتل. ليست مشكلة دقّة، بل خطأ تصنيفي.
والأهمّ: أيّ المقاطع مهمّ يعتمد على الحقل نفسه.
decimals: مُعرَّف في المنشئ، لا يتغيّر أبداً. لا يحتاج طزاجة.owner: لا يتغيّر إلا عبر الحوكمة، قد يبقى ثابتاً سنوات. تحتاج معرفة عند أي block تمّت القراءة (لئلا تنظر إلى قيمة "قبل خمس ترقيات")، لكن لا تحتاج "قبل 23 ثانية".liquidity / slot0: قد تتغيّر مع كل كتلة. الكتلة والوقت كلاهما مهمّ.رقم الكتلة هو المرساة الموضوعيّة الوحيدة — هي الحقيقة على السلسلة، لا تتشوّه باستراتيجيّتنا للذاكرة المؤقّتة.
ما نُشر:
liquidity: 1,234,567 USDC
↳ as of Block #19,234,567 · 23s ago
block للقارئ المتمرّس، و"23s ago" لمن سواه. لا أحدهما يكذب: الكتلة حقيقة فيزيائيّة، و"23s ago" تعني صراحةً الزمن منذ كتابة الذاكرة، وليس "التأخّر عن رأس السلسلة".
عرض متباين حسب سمة الحقل:
# app/services/chain_reader/field_mutability.rb
module ChainReader
module FieldMutability
IMMUTABLE = %w[
name symbol decimals
DOMAIN_SEPARATOR PERMIT_TYPEHASH
factory token0 token1 fee tickSpacing
asset underlying
].freeze
SLOW = %w[
owner
paused deprecated
pauser blacklister masterMinter rescuer
upgradedAddress implementation
maxTotalSupply
].freeze
def self.classify(function_name)
return :immutable if IMMUTABLE.include?(function_name)
return :slow if SLOW.include?(function_name)
:fast
end
end
end
قواعد التصيير:
| الفئة | مثال | العرض |
|---|---|---|
| immutable | decimals, name, symbol |
لا تظهر طزاجة (ضوضاء صرفة) |
| slow | owner, paused |
الكتلة فقط، بلا ثوانٍ |
| fast | slot0, liquidity, totalSupply |
الكتلة + ثوانٍ |
القائمة البيضاء محافِظة عن قصد — أيّ شيء خارجها يُعتبر fast. من الأفضل المبالغة في وسم القيمة كـ "حيّة" على إخفاء قيمة قابلة للتغيّر بصمت بصورة جامدة.
Multicall3 نفسه يوفّر دالّة getBlockNumber(). أضفها في نهاية كل batch — استدعاء RPC واحد، قيمة إضافيّة واحدة، تأخّر يكاد يكون صفراً:
# app/services/chain_reader/multicall3_client.rb
BLOCK_NUMBER_FN = {
"name" => "getBlockNumber",
"inputs" => [],
"outputs" => [{ "type" => "uint256" }]
}.freeze
def call(calls)
block_call = Call.new(target: ADDRESS, function: BLOCK_NUMBER_FN)
augmented = calls + [block_call]
# ...aggregate3 في استدعاء واحد
Batch.new(block_number: block_number, results: results)
end
كذلك ارتفعت نسخة حمولة الذاكرة في ViewCaller (CACHE_VERSION = "v2")، من {fn_sig => result} إلى Snapshot(results:, block_number:, fetched_at:). تفوّض Snapshot عبر def [](key) = @results[key] ومثيلاتها للحفاظ على واجهة hash القديمة — لم يلزم تغيير أي مستدعٍ.
مساعد الواجهة:
def freshness_tag_for(fn)
return nil unless @live_snapshot&.block_number
mutability = ChainReader::FieldMutability.classify(fn["name"])
return nil if mutability == :immutable
block_text = "Block ##{number_with_delimiter(@live_snapshot.block_number)}"
label =
if mutability == :fast && @live_snapshot.fetched_at
"as of #{block_text} · #{freshness_phrase(@live_snapshot.fetched_at)}"
else
"as of #{block_text}"
end
# ...
end
إجمالي الـ PR: 24 ملفاً، +676 / -55.
بعد الإطلاق أعدت كتابة نقطة التميّز:
Smarts هي أوّل وثائق DeFi تخبرك، لكلّ حقل، عند أي block قُرئت حالته.
أكثر تحديداً وأكثر مصداقيّة من "نحن أطرى من Mintlify". إنها تكشف التأخّر الحقيقي بدلاً من إخفائه — منسجمة مع مبدأين أعود إليهما دائماً في CLAUDE.md: "الذكاء الاصطناعي أداة لا واجهة عرض" + "Build in Public".
بعد أكثر من سنة في العمل مع Claude، أكبر استنتاجي ليس سرعته في كتابة الكود — بل أنّه سيجادل خطّتي بجدّيّة إن كلّفت نفسي عناء التوقّف والسؤال.
حين اقترحتُ "إضافة وسم 23s ago"، لم أرَ بنفسي شيئاً خاطئاً. لو راجعت PR الخاصّ بي بنفسي، فالأرجح أنّي كنت سأدمجه — يبدو "صحيحاً بداهةً" أكثر من اللازم.
لكن حين بدّلت النبرة وسألت Claude: "هذا 23s يطرح ماذا من ماذا بالضبط، ويقابل أيّ مقطع تأخّر؟"، نشر السلسلة السببيّة وأظهر لي أنّ المعنى الفيزيائي للرقم خاطئ.
العائد هنا ليس "Claude كتب لي مزيداً من الكود"، بل "Claude منع كذبة منتج من الوصول إلى الإنتاج".
لو نُشر 23s ago فعلاً لما اشتكى أحد — يبدو معقولاً، ومن سيلاحظ الخطأ لن يكلّف نفسه عناء تنبيهك. لكن في عيون من يعرف المجال حقاً، يصبح في موقع توثيق DeFi لديك ثقب: هذا الموقع لا يعرف ما الذي يعرضه.
في المرّة القادمة حين تتّخذ قراراً منتجيّاً مع Claude، خصِّص 5 دقائق لهذه الخطوة:
"الخطّة التي اقترحتها للتو، عدّد ثلاث طرق قد تكون بها خاطئة. لكلّ طريقة سيناريو ملموس."
لو عثر Claude على واحدة لم تخطر ببالك، فقد عادت تلك الدقائق الخمس قيمتها.