Rand Protocol
الوثائق
برامج RISC-V

اكتب برنامجًا بلغة Rust أو C

الدليل الكامل لكتابة برنامجك السري الخاص - ما الذي يستطيع البرنامج فعله وما لا يستطيعه، وكيف تصوغ مدخلاته ومخرجاته، ومزاد بمظاريف مختومة مكتوب بلغتي Rust وC، والاختبار، وميزانية الإثبات، والنشر مع مدخل عام، واستدعاء البرنامج والتحقق من النتيجة.

تغطي هذه الصفحة كل ما تحتاجه لوضع برنامجك الخاص على Rand: ما الذي يمكن أن يكونه البرنامج، وكيف تصمّم ما يقرؤه وما ينشره، ومثالًا كاملًا مكتوبًا مرتين، مرة بلغة Rust ومرة بلغة C. ثم تتناول الاختبار، وميزانية الإثبات، والنشر، والاستدعاء، وكيف يستطيع أي شخص التحقق من النتيجة. كل مخرجات rand-guest أدناه مأخوذة من تشغيل فعلي ومنسوخة كما طُبعت. أُجريت التشغيلات في 2026-09-25 عند الإيداع 7ef3220 من circuits. شبكة testnet هي الآن السلسلة 20، وسلسلة الأدوات الخاصة بها، كما في السلسلتين 18 و19، مبنية عند aeacf31 (الفرع feat/v067-circuits؛ ويثبّت fullnode v0.6.8 الإيداع نفسه)، ولم يُعد تشغيل المخرجات هناك. وتبيّن الصفحة المواضع التي يختلف فيها SDK أو السلسلة.

الدليلان القصيران هما الطريق السريع: نشر برنامج Rust ونشر برنامج RISC-V. تفترض هذه الصفحة أنك أنجزت الخطوة 1 من دليل Rust: نسخة من randprotocol/zkp-circuits عند aeacf31، وRust 1.98.1 مع الهدف riscv32im-unknown-none-elf، والملف التنفيذي rand-guest، وللغة C المترجم clang 23.1.1 مع llvm-tools. مجموعة تعليمات RISC-V هي المرجع للآلة نفسها. ويواصل برامج الأمثلة من حيث تنتهي هذه الصفحة، ببرامج أكبر وبما غيّره قياس الغاز في السلسلة 18. تتناول هذه الصفحة برامج RPL-1 التي ينفذها Call دون حالة؛ وفي السلسلة 20 أيضًا حالة برامج RPL-2 وخلاياها وخزنة تتغير بواسطة Invoke (البرامج)، مع أمثلة كاملة في randprotocol/example-programs.

1. ما الذي يمكن أن يكونه البرنامج

البرنامج هو أي حساب يلائم هذا الشكل:

  • يقرأ كلمات من 32 بت كمدخلات.
  • ينشر ثماني كلمات من 32 بت على الأكثر كمخرجات.
  • هو حتمي: المدخلات نفسها تعطي دائمًا المخرجات نفسها.
  • يتوقف ضمن ميزانية فئته (القسم 7).

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

ما لديكما ليس لديك
تعليمات RV32IM للأعداد الصحيحة، بما فيها MUL/DIV/REMعتاد للفاصلة العائمة (الأعداد العائمة البرمجية في Rust تعمل، ببطء)
حتى 1 MiB من الذاكرة ابتداءً من 0x1000 في نص الربط الافتراضي، مع مكدس بحجم 64 KiBكومة، ما لم توفّر مخصِّصًا للذاكرة (القسم 6)
Poseidon2 وKeccak-f وSHA-256 كاستدعاءات نظامالملفات، والشبكة، والساعات، والعشوائية، والخيوط، وstd، وlibc
حزم Rust من نوع no_std وC مكتفية بذاتهاحالة تبقى من استدعاء إلى آخر
ثماني كلمات مخرجات عامةوسيلة لنقل RAND أو الرموز من داخل البرنامج

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

2. صمّم الواجهة أولًا

للبرنامج ثلاث قنوات. اختر ما يوضع في كل منها قبل أن تكتب أي شيفرة.

القناةمتى تُحدَّدتُقرأ بواسطةمن يراهاالحد في السلسلة 20
مدخل خاصفي كل استدعاءread_input(i) / rand_read_input(i)المستدعي وحده، ومن يسلّمه مفتاح الاستدعاء16,071 كلمة (سقف مغلّف المدخلات المختوم)
المقطع العاممرة واحدة، عند النشرread_public(i) / rand_read_public(i)الجميع؛ ويرتبط به معرّف البرنامج32,768 كلمة
المخرجاتعند كل تشغيلwrite_output(slot, w) / rand_write_outputالجميع، في الإيصال8 كلمات

المدخلات الخاصة لا تثبت شيئًا عن العالم بمفردها

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

  • التزام نُشر سابقًا (في المقطع العام، أو سُلّم إلى المتحقق خارج القناة) ويجب أن يفتحه المدخل الخاص، كما في المزاد أدناه؛
  • جذر Merkle يجب إثبات ورقة خاصة تحته (Poseidon2 هو استدعاء النظام المخصص لهذا)؛
  • بصمة يحتفظ بها المتحقق أصلًا، يعيد البرنامج حسابها وينشرها كمخرج.

هذه هي المهارة التصميمية المحورية للبرامج السرية. يخبر الإثبات المتحقق ما الذي حُسب. ويخبره الربط بالبيانات العامة على أي شيء حُسب.

ترميز البيانات في كلمات

كل شيء ينتقل بصيغة u32:

البياناتالترميز
سلسلة بايتاتكلمة طول، ثم البايتات أربعة في كل كلمة بترتيب little-endian، وتُحشى الكلمة الأخيرة بالأصفار (الصيغة التي تستخدمها المحفظة لمدخل ELF العام)
u64 أو مبلغكلمتان، الدنيا ثم العليا
عدد صحيح بإشارةالمتمم الثنائي في كلمة واحدة
نتيجة كبيرةبصمتها Poseidon2: 8 كلمات، وهي تملأ كل المخرجات؛ أو انشر بضع كلمات ملخِّصة والتزامًا أقصر

تأخذ أدوات سطر الأوامر كلمات المدخلات بالنظام العشري: rand-guest run --input وrand call --input لا تقبلان 0x…. أما ملف المقطع العام لـrand program deploy --public فيقبل العشري والستة عشري 0x معًا.

شكّل المخرجات

  • الخانة 0 كلمة حالة. أعطِ كل نتيجة رقمًا، بما فيها حالات الفشل: فتح لا يطابق، أو عدد عناصر زائد، أو مدخل خارج النطاق. ينبغي أن ينتهي التحقق الفاشل بتوقف سليم مع حالة، ليحصل المستدعي على إثبات يبيّن ما الذي أخفق.
  • تُكتب كل خانة مرة واحدة على الأكثر. الكتابة الثانية في الخانة نفسها فخ (DoubleWrite(0) في المحاكي)، والتشغيل الذي يقع في فخ لا يمكن إثباته. والخانات التي لا تكتبها تُقرأ صفرًا.
  • في Rust، أبقِ الخانة 7 حرة. يكتب guest بلغة Rust عند حدوث panic القيمة 0xdeadbeef (3,735,928,559) في الخانة 7 ثم يتوقف. وتبقى المخرجات التي كتبها قبل الـpanic في مكانها، لذا يجب على القارئ أن يفحص الخانة 7 قبل أن يصدّق الباقي (القسم 6).
  • القراءة بعد آخر مدخل مُعطى فخ (InputIndex(5) في المحاكي). اقرأ العدد أولًا وتحقق منه.

3. المثال التطبيقي: مزاد بعطاءات مختومة

يريد بائع إجراء مزاد بالسعر الثاني دون نشر العطاءات الخاسرة:

  1. يختار كل مزايد عطاءً وملحًا عشوائيًا من 128 بت، ويعطي البائع التزامًا: Poseidon2 على ("BID1", bidder index, bid, salt).
  2. ينشر البائع برنامج المزاد مع سعر الاحتياطي وكل الالتزامات بوصفها مقطعه العام. يسمّي معرّف البرنامج الآن هذا المزاد وحده.
  3. يرسل كل مزايد عطاءه وملحه إلى البائع بشكل خاص.
  4. يستدعي البائع البرنامج وكل عطاء وملح مدخلًا خاصًا. يتحقق البرنامج من كل فتح مقابل التزامه، ويختار الفائز، وينشر [status, winner, price, bidder count].

تعرف السلسلة من فاز وبأي سعر. ولا ترى العطاءات الخاسرة أبدًا. وإذا غيّر البائع أي عطاء حصل على BAD_OPENING بدل النتيجة، لأن الالتزامات ثابتة في معرّف البرنامج.

يقع كل شيء داخل نسخة circuits، لأن rand-guest build يجد guest-sdk بالبحث صعودًا من مجلد الـguest:

circuits/
  guest-sdk/
  rand-guest/
  my-guests/
    commit/          a Rust guest that computes one commitment
    auction-core/    the auction rule as a plain no_std library, tested on the host
    auction/         the Rust guest
    auction-c/       the same guest in C
    auction-c-test/  a host test for the C rule, outside the guest's directory

المحاكي بوصفه حاسبة بصمات

يحتاج المزايدون إلى Poseidon2 على حقل Goldilocks، تمامًا كما تحسبه الآلة. والأداة الأبسط والصحيحة هي guest صغير يحسبه، يُشغَّل على المحاكي. البصمة ثماني كلمات، وهو بالضبط عدد خانات المخرجات.

my-guests/commit/Cargo.toml:

[package]
name = "commit"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

my-guests/commit/src/main.rs:

#![no_std]
#![no_main]

use guest_sdk::{halt, poseidon2, read_input, write_output};

/// Inputs: a bidder's index, bid and four salt words.
/// Outputs: the 8-word commitment Poseidon2("BID1", index, bid, salt[0..4]).
#[no_mangle]
pub extern "C" fn main() -> ! {
    let mut buf = [0u32; 8];
    buf[0] = 0x4249_4431; // "BID1"
    for i in 0..6 {
        buf[1 + i] = read_input(i as u32);
    }
    unsafe { poseidon2(buf.as_mut_ptr(), 7) };
    for (slot, word) in buf.iter().enumerate() {
        write_output(slot as u32, *word);
    }
    halt();
}

تقوم poseidon2(ptr, n) بتجزئة n كلمة في مكانها وتكتب البصمة ذات الكلمات الثماني فوق ptr..ptr+8، لذا يجب أن تتسع الذاكرة المؤقتة لثماني كلمات على الأقل مع أن سبعًا فقط تُجزَّأ. ويُبقي الوسم "BID1" هذه البصمات منفصلة عن أي استخدام آخر لـPoseidon2.

عند aeacf31 تكون poseidon2 دالة unsafe fn لأنها تأخذ مؤشرًا خامًا، فيقع الاستدعاء في كتلة unsafe. أما عند 7ef3220، حيث أُنتجت المخرجات أدناه، فكانت دالة آمنة وكُتب السطر نفسه دون الكتلة. والإسفنجة لا تحشو: [a] و[a, 0] يعطيان التجزئة نفسها. وهذا غير ضار هنا، إذ كل رسالة سبع كلمات وتبدأ بوسم. أما لرسالة يمكن أن يتغير طولها، ففي SDK عند aeacf31 الدالة poseidon2_len(buf, n) التي تربط الطول (استدعاء النظام 7)، لكن rand-guest check عند ذلك الإيداع يرفض استدعاء النظام 7، فلا يُبنى مثل هذا الـguest بعد (المسألة #1 في zkp-circuits). ضع الطول داخل الرسالة بدلًا من ذلك.

$ rand-guest build my-guests/commit --out commit.bin
76 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote commit.bin and its .sha256 (76 words, hc 41007891b5c393296fdd76539c93d0b06678bc8a0bd52cfc3b6886dcf0381343, program id 11dabdf740989d43599b296c43342bf3ce890e980f5cc558d15ba8ca9e8e9438)

$ rand-guest run commit.bin --input 0 --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
out[0] = 3502385358
out[1] = 1187922325
out[2] = 2500412136
out[3] = 2452821097
out[4] = 2045657999
out[5] = 1866863173
out[6] = 678782893
out[7] = 3524569252
cycles 77
tier 10

حُذفت أسطر cargo من مخرجات البناء هنا وفيما يلي. والتحذير unaligned-scalar-mem الذي يطبعه cargo متوقع (نشر برنامج Rust).

أربعة مزايدين، بهذه العطاءات والأملاح:

المزايدالعرضالملح (أربع كلمات)
04002654435769 2135587861 4087136352 1559071514
1250461845907 3432918353 3864292196 2246822507
29003266489909 668265263 374761393 3550635116
33004252018373 3042594569 1759714724 625341585

تشغيل commit.bin مرة واحدة لكل مزايد، ثم كتابة السعر الاحتياطي (100) والعدد (4) والملخصات الأربعة بالترتيب، يعطي المقطع العام auction-public.txt بطول 34 كلمة:

100 4 3502385358 1187922325 2500412136 2452821097 2045657999 1866863173 678782893 3524569252 3413403069 1515911705 3633767467 1218237979 474331206 896634170 2889783686 541804293 3111308722 99534202 1917186958 1017842926 3082732585 4215265588 467906960 2929273893 588931287 1295911076 647129560 1384102451 625140897 4060943258 2970944086 698558155

في مزاد حقيقي يشغّل كل مزايد commit.bin على جهازه، ويأتي الملح من مصدر عشوائي حقيقي.

4. البرنامج بلغة Rust

القاعدة بوصفها مكتبة يمكنك اختبارها

ضع المنطق الذي لا يحتاج إلى استدعاءات النظام في حزمة no_std مستقلة. ترفض guest-sdk البناء لأي هدف غير riscv32، ولذلك لا يمكن تشغيل cargo test لحزمة ضيف على حاسوبك. أما حزمة مكتبة لا تعتمد على guest-sdk فيمكن.

my-guests/auction-core/Cargo.toml:

[package]
name = "auction-core"
version = "0.1.0"
edition = "2021"

[workspace]

my-guests/auction-core/src/lib.rs:

//! The auction's rule, with no syscalls, so it builds and tests on the host.
#![no_std]

#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
    /// The highest bidder wins and pays the second-highest bid, or the reserve if higher.
    Sold { winner: u32, price: u32 },
    /// No bid reached the reserve.
    NoSale,
}

/// A second-price (Vickrey) auction. Ties go to the lower index.
pub fn settle(reserve: u32, bids: &[u32]) -> Outcome {
    let mut best: Option<(u32, u32)> = None; // (index, bid)
    let mut second = 0u32;
    for (i, &bid) in bids.iter().enumerate() {
        match best {
            Some((_, top)) if bid <= top => second = second.max(bid),
            Some((_, top)) => {
                second = top;
                best = Some((i as u32, bid));
            }
            None => best = Some((i as u32, bid)),
        }
    }
    match best {
        Some((winner, top)) if top >= reserve => Outcome::Sold { winner, price: second.max(reserve) },
        _ => Outcome::NoSale,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn highest_bid_pays_the_second() {
        assert_eq!(settle(100, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 400 });
    }

    #[test]
    fn the_reserve_sets_a_floor() {
        assert_eq!(settle(500, &[400, 250, 900, 300]), Outcome::Sold { winner: 2, price: 500 });
        assert_eq!(settle(1000, &[400, 250, 900, 300]), Outcome::NoSale);
    }

    #[test]
    fn a_tie_goes_to_the_lower_index() {
        assert_eq!(settle(0, &[700, 700]), Outcome::Sold { winner: 0, price: 700 });
    }

    #[test]
    fn no_bids_is_no_sale() {
        assert_eq!(settle(0, &[]), Outcome::NoSale);
    }
}
$ cd my-guests/auction-core && cargo +1.98.1 test
running 4 tests
test tests::a_tie_goes_to_the_lower_index ... ok
test tests::highest_bid_pays_the_second ... ok
test tests::no_bids_is_no_sale ... ok
test tests::the_reserve_sets_a_floor ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

الضيف

يعتمد my-guests/auction/Cargo.toml على الحزمتين عبر المسار:

[package]
name = "auction"
version = "0.1.0"
edition = "2021"

[dependencies]
guest-sdk = { path = "../../guest-sdk" }
auction-core = { path = "../auction-core" }

[profile.release]
opt-level = 3
lto = true
panic = "abort"
codegen-units = 1

[workspace]

my-guests/auction/src/main.rs:

#![no_std]
#![no_main]

use auction_core::{settle, Outcome};
use guest_sdk::{halt, poseidon2, read_input, read_public, write_output};

const MAX_BIDDERS: usize = 64;
const TAG: u32 = 0x4249_4431; // "BID1", the same tag the commit guest uses

// Output slot 0: what happened.
const SOLD: u32 = 1;
const NO_SALE: u32 = 2;
const BAD_OPENING: u32 = 3;
const TOO_MANY: u32 = 4;

/// Public (fixed at deploy): reserve, n, then n 8-word commitments.
/// Private (per call): for each bidder, the bid and four salt words.
/// Outputs: [status, winner, price, n, 0, 0, 0, 0]; a panic sets slot 7 to 0xdeadbeef.
#[no_mangle]
pub extern "C" fn main() -> ! {
    let reserve = read_public(0);
    let n = read_public(1) as usize;
    if n > MAX_BIDDERS {
        finish(TOO_MANY, 0, 0, n as u32);
    }

    let mut bids = [0u32; MAX_BIDDERS];
    for i in 0..n {
        let base = (5 * i) as u32;
        let bid = read_input(base);
        let mut buf = [TAG, i as u32, bid, read_input(base + 1), read_input(base + 2),
                       read_input(base + 3), read_input(base + 4), 0];
        unsafe { poseidon2(buf.as_mut_ptr(), 7) };
        let committed = 2 + 8 * i as u32;
        for w in 0..8 {
            if buf[w] != read_public(committed + w as u32) {
                finish(BAD_OPENING, i as u32, 0, n as u32);
            }
        }
        bids[i] = bid;
    }

    match settle(reserve, &bids[..n]) {
        Outcome::Sold { winner, price } => finish(SOLD, winner, price, n as u32),
        Outcome::NoSale => finish(NO_SALE, 0, 0, n as u32),
    }
}

fn finish(status: u32, winner: u32, price: u32, n: u32) -> ! {
    write_output(0, status);
    write_output(1, winner);
    write_output(2, price);
    write_output(3, n);
    halt();
}

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

البناء والتشغيل

$ rand-guest build my-guests/auction --out auction.bin
258 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction.bin and its .sha256 (258 words, hc aa04e4c451e986f086e9fcff53b213eb52e593317c32a979d5e24991164f050d, program id 148ab0c4b9ad5115caab4a2ab433e3e13b5e8fbecbd9d8a9d6ba20c64f942c73)

يأخذ rand-guest run المقطع العام في صورة أعلام --public متكررة. في الصدفة:

PUB=$(for w in $(cat auction-public.txt); do printf -- "--public %s " "$w"; done)
BIDS="--input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514
      --input 250 --input 461845907 --input 3432918353 --input 3864292196 --input 2246822507
      --input 900 --input 3266489909 --input 668265263 --input 374761393 --input 3550635116
      --input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585"
rand-guest run auction.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 641
tier 10
tier 10: fits (cycles 721 of 1023, Poseidon2 permutations 88 of 128)

بيع (1) إلى المزايد 2 بسعر 400، وهو ثاني أعلى عرض، مع احتساب أربعة مزايدين. وفيما يلي تشغيلان ينبغي أن يفشلا:

التغييرالمخرجات 0–3المعنى
استبدال عرض المزايد 1 البالغ 250 بالقيمة 2603 1 0 0BAD_OPENING عند المزايد 1؛ ولا يُنشر شيء آخر
سعر احتياطي 1,000 بدلًا من 100 (مقطع عام مختلف، ومن ثم معرّف برنامج مختلف)2 0 0 4NO_SALE: لم يبلغ أي عرض السعر الاحتياطي

5. البرنامج نفسه بلغة C

يضم دليل ضيف C ملفات .c وترويساتك الخاصة. يوفّر rand-guest build --lang c الملف guest.h و_start ومكتبة تشغيل صغيرة (نشر برنامج RISC-V). ويُصرّف كل ملف .c في الدليل، ولذلك ينبغي أن توضع ملفات اختبار المضيف في مكان آخر.

my-guests/auction-c/auction.h:

#ifndef AUCTION_H
#define AUCTION_H
#include <stdint.h>

/* Outcome of a second-price auction. sold is 0 when no bid reached the reserve. */
struct outcome {
    uint32_t sold;
    uint32_t winner;
    uint32_t price;
};

struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n);
#endif

my-guests/auction-c/settle.c:

#include "auction.h"

/* A second-price (Vickrey) auction. Ties go to the lower index. */
struct outcome settle(uint32_t reserve, const uint32_t *bids, uint32_t n) {
    struct outcome o = {0, 0, 0};
    if (n == 0) {
        return o;
    }
    uint32_t winner = 0, top = bids[0], second = 0;
    for (uint32_t i = 1; i < n; i++) {
        if (bids[i] > top) {
            second = top;
            top = bids[i];
            winner = i;
        } else if (bids[i] > second) {
            second = bids[i];
        }
    }
    if (top >= reserve) {
        o.sold = 1;
        o.winner = winner;
        o.price = second > reserve ? second : reserve;
    }
    return o;
}

my-guests/auction-c/main.c:

#include "guest.h"
#include "auction.h"

#define MAX_BIDDERS 64
#define TAG 0x42494431u /* "BID1", the same tag the commit guest uses */

enum { SOLD = 1, NO_SALE = 2, BAD_OPENING = 3, TOO_MANY = 4 };

static uint32_t bids[MAX_BIDDERS];

static void finish(uint32_t status, uint32_t winner, uint32_t price, uint32_t n) {
    rand_write_output(0, status);
    rand_write_output(1, winner);
    rand_write_output(2, price);
    rand_write_output(3, n);
    rand_halt();
}

/* Public (fixed at deploy): reserve, n, then n 8-word commitments.
 * Private (per call): for each bidder, the bid and four salt words.
 * Outputs: [status, winner, price, n, 0, 0, 0, 0]. */
void main(void) {
    uint32_t reserve = rand_read_public(0);
    uint32_t n = rand_read_public(1);
    if (n > MAX_BIDDERS) {
        finish(TOO_MANY, 0, 0, n);
    }
    for (uint32_t i = 0; i < n; i++) {
        uint32_t base = 5 * i;
        uint32_t buf[8] = {TAG, i, rand_read_input(base), rand_read_input(base + 1),
                           rand_read_input(base + 2), rand_read_input(base + 3),
                           rand_read_input(base + 4), 0};
        bids[i] = buf[2];
        rand_poseidon2(buf, 7);
        for (uint32_t w = 0; w < 8; w++) {
            if (buf[w] != rand_read_public(2 + 8 * i + w)) {
                finish(BAD_OPENING, i, 0, n);
            }
        }
    }
    struct outcome o = settle(reserve, bids, n);
    if (o.sold) {
        finish(SOLD, o.winner, o.price, n);
    }
    finish(NO_SALE, 0, 0, n);
}

bids متغير static مهيّأ بالأصفار، فيوضع في .bss. ولا يكلّف ذلك أي كلمات في الصورة، لأن الذاكرة تُقرأ أصلًا أصفارًا.

اختبار القاعدة على المضيف

لا يستخدم settle.c أي استدعاء نظام، ولذلك يمكن لمصرّفك المعتاد أن يبنيه. my-guests/auction-c-test/test_settle.c:

#include <assert.h>
#include <stdio.h>
#include "../auction-c/auction.h"

int main(void) {
    uint32_t bids[] = {400, 250, 900, 300};
    struct outcome o = settle(100, bids, 4);
    assert(o.sold && o.winner == 2 && o.price == 400);
    o = settle(500, bids, 4);
    assert(o.sold && o.price == 500);
    o = settle(1000, bids, 4);
    assert(!o.sold);
    uint32_t tie[] = {700, 700};
    o = settle(0, tie, 2);
    assert(o.winner == 0 && o.price == 700);
    puts("settle: 4 checks passed");
    return 0;
}
$ cc -Wall -o test_settle my-guests/auction-c-test/test_settle.c my-guests/auction-c/settle.c && ./test_settle
settle: 4 checks passed

البناء والتشغيل

$ rand-guest build --lang c my-guests/auction-c --out auction-c.bin
200 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
OK
wrote auction-c.bin and its .sha256 (200 words, hc 68605aa8d28a863a4a52a25de07ba159327a0fd0145a9dcd5e5aa6e79ab6bea0, program id 5667c343b8848805a908c9009d426164f39d2f927b2c780529a054f0d66aafc1)

$ rand-guest run auction-c.bin $PUB $BIDS --tier 10
out[0] = 1
out[1] = 2
out[2] = 400
out[3] = 4
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 0
cycles 576
tier 10
tier 10: fits (cycles 641 of 1023, Poseidon2 permutations 73 of 128)

تطابق المخرجات مخرجات ضيف Rust. ومع أربعة مزايدين، تكون صورة C أصغر بـ 58 كلمة وأسرع بـ 65 دورة؛ ويبيّن القسم 7 أن الكفة تميل عند 64 مزايدًا. والبرنامجان مختلفان، ولكل منهما hc ومعرّف برنامج مختلفان. انشر الذي تنشر شيفرته المصدرية.

6. ملاحظات حول اللغات

Rust

الحزمة. #![no_std] و#![no_main] ودالة #[no_mangle] pub extern "C" fn main() -> ! تنتهي بـ halt(). يضبط ملف الإصدار panic = "abort". ويُبقي جدول [workspace] الفارغ الحزمة خارج أي مساحة عمل أب. لا تضف .cargo/config.toml ولا أعلام مصرّف خاصة بك: فـ rand-guest build يرفض الأول ويضبط كل الثاني (نشر برنامج Rust).

حالات الذعر (panic). يكتب معالج الذعر في guest-sdk القيمة 0xdeadbeef في الخانة 7 ثم يوقف التنفيذ. ويبقى التشغيل صالحًا وقابلًا للإثبات. يقرأ هذا البرنامج الفهرس 999 من Vec ذي ستة عناصر بعد كتابة الخانتين 0 و1:

$ rand-guest run heap.bin --input 6 --input 1 --input 2 --input 3 --input 4 --input 5 --input 6
out[0] = 4
out[1] = 1080033280
out[2] = 0
out[3] = 0
out[4] = 0
out[5] = 0
out[6] = 0
out[7] = 3735928559
cycles 1326
tier 12

تحمل الخانتان 0 و1 قيمًا حقيقية، وتدل الخانة 7 على أن التشغيل أصابه ذعر. والمستدعي الذي يقدّم تشغيلًا كهذا يحصل على إيصال على السلسلة. وعلى من يقرأ الإيصالات أن يرفض الخانة 7 = 3,735,928,559، أو أن يُكتب البرنامج بحيث لا يُصاب بالذعر أبدًا. فضّل get() والحساب المفحوص وأكواد الحالة الصريحة في كل مسار.

الكومة. لا يوجد مخصّص ذاكرة افتراضيًا. تعمل alloc متى وفّرت واحدًا. ويناسب البرنامج الذي يعمل مرة واحدة ثم ينتهي مخصّص تصاعدي (bump) فوق ساحة ثابتة الحجم:

extern crate alloc;

use alloc::vec::Vec;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;

/// A bump allocator over a fixed arena: allocation moves a pointer forward, free does nothing.
/// One run is one short-lived process, so nothing needs to be reclaimed.
struct Bump {
    arena: UnsafeCell<[u8; ARENA]>,
    next: UnsafeCell<usize>,
}
const ARENA: usize = 256 * 1024;
unsafe impl Sync for Bump {}

unsafe impl GlobalAlloc for Bump {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let next = &mut *self.next.get();
        let start = (*next + layout.align() - 1) & !(layout.align() - 1);
        let end = start + layout.size();
        if end > ARENA {
            return core::ptr::null_mut();
        }
        *next = end;
        (self.arena.get() as *mut u8).add(start)
    }
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {}
}

#[global_allocator]
static HEAP: Bump = Bump { arena: UnsafeCell::new([0; ARENA]), next: UnsafeCell::new(0) };

الساحة مصفّرة، فتوضع في .bss ولا تضيف كلمات إلى الصورة. يمنح نص الربط الافتراضي 1 MiB من الذاكرة، ويجب أن تتسع الساحة والمتغيرات الساكنة والمكدس البالغ 64 KiB داخلها. ومع هذا المخصّص تُبنى Vec وcollect() وiter().sum() كلها وتجتاز check. غير أن alloc وتبعياتها تضيف شيفرة: فحجم برنامج الاختبار هذا 1,341 كلمة، مقابل 258 للمزاد. ويشرح القسم 7 لماذا يكلّف حجم البرنامج من ميزانية الإثبات.

الفاصلة العائمة. لا تحتوي المعمارية المستهدفة على تعليمات عائمة، وتحوّل Rust النوعين f32/f64 إلى روتينات برمجية. وهي تعمل: فمتوسط 9 و2 و7 و4 و5 المحسوب بـ f64 عاد في صورة بتات 5.4f32. لكن كلًّا منها يكلّف دورات كثيرة. استخدم الأعداد الصحيحة أو الفاصلة الثابتة حيث أمكن.

الحزم. تُبنى حزم no_std من crates.io مثل أي تبعية. وقد طبعت دالة CRC-32 من حزمة crc للنص hello, rand، عند تشغيلها على المحاكي، القيمة 2124296908، وهي القيمة نفسها التي يعطيها zlib.crc32 في Python. أما الحزم التي تحتاج إلى std فلا تُبنى لهذا الهدف. ولا يمرّر rand-guest build العلم --locked، لذا أودِع ملف Cargo.lock في المستودع: فبدونه قد تحلّ إعادة بناء بعد أشهر نسخة أحدث من حزمة وتنتج hc مختلفًا.

فخ unimp: نتيجة Csr من شيفرة مكتبة. تُصرَّف بعض شيفرات core إلى كلمة unimp (0xc0001073، أي csrrw x0, cycle, x0) على مسار يعدّه LLVM غير قابل للوصول. وليس في الآلة أي سجلات CSR، ولذلك يرفض check الصورة:

0x00002250  0xc0001073  Csr: CSR instruction: the machine has no CSRs
2613 words against a cap of 4096 (fits); 0 ecall(s) with a non-static a7
REJECTED
wrote heap.bin (the loader refuses it: no hc)

كان المسبّب هنا slice::sort_unstable على Vec لا يُعرف طوله إلا عند التشغيل. وأنتج sort() المستقر الأثر نفسه، بينما اجتاز sort_unstable على مصفوفة ثابتة من 16 عنصرًا. ولتحديد مصدر نتيجة كهذه، فكّك ملف ELF للضيف وانظر إلى الدالة المحيطة بالعنوان الذي طبعه check:

llvm-objdump -d -C my-guests/heap/target/riscv32im-unknown-none-elf/release/heap | grep -B12 unimp

ثم استبدل ذلك الاستدعاء بشيفرة من عندك (وهي هنا فرز بالإدراج قصير). وللسبب نفسه تنتهي halt() في guest-sdk بـ loop {} بدلًا من وعد noreturn.

C

  • نقطة الدخول هي void main(void)، وتنتهي بـ rand_halt(). يضبط _start المرفق المكدس ثم يستدعيها.
  • لا توجد libc. لا printf ولا malloc ولا <string.h>. توفّر مكتبة التشغيل memcpy وmemmove وmemset وmemcmp (يولّد clang استدعاءات لها عند نسخ البنى والتهيئات الكبيرة) ودوال القسمة الأربع بحجم 64 بت. وجميعها حلقات بايتات، فانسخ واقسم بالكلمات في الشيفرة الساخنة. وللكومة اكتب مخصّصًا تصاعديًا فوق مصفوفة static، كما في Rust.
  • السلوك غير المعرَّف لا يُكتشف. لا يوجد معالج ذعر. فالكتابة خارج الحدود تفسد الذاكرة بصمت، والتحميل أو التخزين غير المحاذى يسبب فخًّا، ما يعني أن التشغيل لا يمكن إثباته. تحقق من الفهارس مقابل حدودك الخاصة، كما يتحقق main.c من n مقابل MAX_BIDDERS.
  • قواعد الدليل. لا تضف guest.h ولا start.S من عندك؛ فـ build --lang c يرفضهما. وأبقِ ملفات الاختبار خارج دليل الضيف، لأن كل ملف .c فيه يُصرَّف داخل الصورة.
  • clang مثبّت. يرفض rand-guest أي clang غير 23.1.1. ويتجاوز RAND_GUEST_CLANG_UNPINNED=1 ذلك، مع تحذير بأن hc الخاص بك لن يطابق بناء أي شخص آخر.

7. هل سيُثبت؟ الميزانية

يملأ المُثبِت (prover) كل تشغيل حتى مستوى t. وللمستوى ميزانيتان مستقلتان، ويبلّغ rand-guest run --tier t عنهما معًا:

  • الصفوف: بحد أقصى 2ᵗ − 1، وتشمل الدورات المنفّذة وصفوف الملخصات؛
  • تبديلات Poseidon2: بحد أقصى 2ᵗ⁻³.
المستوىالصفوفتبديلات Poseidon2سقف الغاز بلا جدول تجزئةاستدعاء على السلسلة 20
101,0231281,279مقبول
124,0955125,119مقبول
1416,3832,04820,479مقبول
1665,5358,19281,919مرفوض منذ v0.5.6
18, 202¹⁸ − 1, 2²⁰ − 12¹⁵, 2¹⁷327,679, 1,310,719مرفوض منذ v0.5.6

الرسم يتبع الغاز والبايتات، لا المستوى. على السلسلة 15 كان الاستدعاء يدفع رسمًا ثابتًا قدره 0.001 + call_fee(tier): 0.002 RAND عند المستوى 10، و0.0021 عند المستوى 12، و0.0022 عند المستوى 14. ومنذ السلسلة 18 صار كل تشغيل مقيسًا. الصف العادي يساوي 1 غاز، وصف امتصاص Poseidon2 يساوي 3، وتبديل Keccak يساوي 192، وضغط SHA-256 يساوي 64. ويعلن الإثبات حد غاز مصرَّحًا به، وتفرض الدارة أن يكون غاز التشغيل مساويًا له أو دونه، ويبقى الغاز الفعلي سرّيًا. والسقف الذي يمكن للمستوى أن يصرّح به بلا جدول تجزئة هو (2ᵗ − 1) + 2ᵗ⁻²، أي العمود أعلاه. أما الحد الأدنى للرسم فهو

floor = BUNDLE_BASE + gas_price × declared gas limit + byte_price × ⌈bytes / 1024⌉

حيث BUNDLE_BASE يساوي 0.001 RAND، والبايتات هي إثبات الاستدعاء مضافًا إليه غلاف المدخلات، والأسعار بدأت عند 100 وحدة لكل غاز و800 وحدة لكل KiB (الوحدة 10⁻⁹ RAND) وتتحرك قليلًا في كل كتلة. وعلى السلسلة 20 لا يمكنها أن ترتفع فوق 10,000 و80,000 وحدة، ولا يتحرك سعر البايت إلا ببايتات إثبات الاستدعاءات وأغلفتها؛ أما على السلسلتين 18 و19 فلم يكن هناك سقف. ويبيّن القسم 9 ما تدفعه المحفظة افتراضيًا، وفي برامج مثال أربعة استدعاءات مقيسة على السلسلة 18، بكلفة 0.0057 إلى 0.0075 RAND لكل منها.

المستوى 14 هو أعلى مستوى يمكن أن يستخدمه الاستدعاء. منذ fullnode v0.5.6 (2026-09-25) يرفض العقدة أي إثبات استدعاء يتجاوز المستوى 14 برمز الخطأ CallTierTooHigh، وذلك قبل أن تبني مفتاح المُتحقِّق. وبالقاعدة نفسها تصبح الكتلة التي تحمل استدعاءً كهذا غير صالحة. يحمي هذا الحد المدقِّقين الذين تبلغ ذاكرتهم 2–4 غيغابايت: فبناء مفتاح مُتحقِّق للمستوى 20 يستغرق دقائق ويستهلك غيغابايتات. لا يرفض rand call استدعاءً كهذا قبل أن يُنشئ الإثبات، لكنه يطبع المستوى أولًا على السلسلة 20. شغِّل rand-guest run … --tier 14 قبل أن تستدعي، وإن لم يكن التشغيل ضمن الحد فأعد هيكلة البرنامج. وقبل النشر، يشغّل rand program deploy --input <word> … (أو --check-call لاستدعاء بلا مدخلات) الاستدعاء الذي تنوي إجراءه عبر المحاكي، ويرفض النشر ما لم يصل إلى المستوى 14 أو أدنى منه.

ما الذي يستهلك الميزانية

صفوف الملخّصات والتبديلات هي المفاجأة. قبل أن يعمل شيفرتك، تُجري الآلة تجزئة للبرنامج وللمدخلات الخاصة وللمقطع العام لتنتج hc وH_IN وH_PUB. تكلّف كل واحدة منها نحو تبديل واحد وصفًّا واحدًا لكل أربع كلمات. وفوق ذلك، يكلّف كل استدعاء لـ POSEIDON2 نحو تبديل واحد لكل أربع كلمات تُجزَّأ. في مثال المزاد عند المستوى 10:

البندالكلماتالتبديلات
البرنامج (hc)258نحو 65
المدخلات الخاصة (H_IN)20نحو 5
المقطع العام (H_PUB)34نحو 9
أربعة فحوص التزام، 7 كلمات لكل منها4 × 78
المجموع كما يعرضه run88 من 128

الصفوف: 641 دورة مع صفوف الملخّصات بلغت 721 من 1,023.

على السلسلة 20، كما على السلسلة 19، يكون عدّ المحفظة نفسها أعلى قليلًا من عدّ rand-guest run. فإثبات الاستدعاء هناك مرتبط بمعاملته (وعلى السلسلة 20 يشمل الربط أيضًا تجزئة التكوين الأول genesis، بالكلمات الثماني نفسها)، ويُنقل الربط في صورة ثماني كلمات إضافية من المقطع العام: صفّان إضافيان للملخّصات وتبديلان إضافيان لبرنامج له مقطع عام، وصفّ واحد وتبديل واحد لبرنامج بلا مقطع عام.

وهذا يجعل حجم البرنامج جزءًا من الميزانية. فبرنامج heap الاختباري لم يتجاوز تشغيله 1,183 دورة، لكن كلماته البالغة 1,341 استهلكت 340 تبديلًا ودفعته إلى المستوى 12. وعند 2,048 تبديلًا للمستوى 14، لا يمكن استدعاء برنامج يزيد كثيرًا على نحو 8,000 كلمة مهما فعل. وعلى السلسلة 20 يبلغ الحد الدقيق 8,180 كلمة لبرنامج بلا مقطع عام واستدعاء بلا مدخلات، ويقلّ عن ذلك مع أيٍّ منهما. سقف البرنامج على السلسلة 20 هو 65,535 كلمة، كما كان على السلسلة 15، لكن السلسلتين 19 و20 ترفضان نشر برنامج يتجاوز هذا الحد (ProgramUncallable)، بينما كانت السلسلة 15 تأخذ رسم النشر عن برنامج لا يستطيع أحد استدعاءه. ولهذا لا يمكن نشر ERC-20 المترجَم من السلسلة 13 (11,686 كلمة، واستُدعي عند المستوى 16) ولا استدعاؤه على السلسلة 20. أبقِ البرامج صغيرة: opt-level = "s" في Rust، و-Os (مضبوط أصلًا) في C، وقلّة من الاعتماديات، ولا alloc إلا عند الحاجة.

يتزايد المزاد خطيًّا: كل مزايد إضافي يضيف 5 كلمات مدخلات و8 كلمات عامة وتبديلين للفحص ونحو مئة دورة. مع 64 مزايدًا (عروض عشوائية، 320 كلمة مدخلات، 514 كلمة عامة) يختار البناءان الفائز والسعر نفسيهما، ويحتاج كلاهما إلى المستوى 14:

auction.bin    tier 14: fits (cycles 6557 of 16383, Poseidon2 permutations 403 of 2048)
auction-c.bin  tier 14: fits (cycles 8571 of 16383, Poseidon2 permutations 388 of 2048)

كانت التبديلات ستلائم 512 للمستوى 12، لكن الصفوف لا تلائم 4,095 منه. فالدورات، لا التجزئة، هي التي تحدد المستوى هنا. عند 64 مزايدًا يكون بناء Rust هو الأسرع. فإعداده opt-level = 3 يتفوق على -Os الثابت في بناء C متى هيمنت الحلقة.

Keccak وSHA-256

يكلّف كل من keccak وsha256_compress صفًّا واحدًا، لكن الإثبات الذي يستخدم أيًّا منهما يحمل جدولًا إضافيًّا. عند aeacf31 يأخذ كلاهما مرجع مصفوفة، keccak(&mut state) على [u32; 50] وsha256_compress(&mut buf) على [u32; 24]، بينما كان 7ef3220 يأخذ مؤشرًا خامًا. ومنذ v0.5.6 لا يجوز للاستدعاء أن يستخدم أكثر من 128 تبديل Keccak-f وأكثر من 128 ضغطة SHA-256. وعلى السلسلة 20 يكلّف تبديل Keccak أيضًا 192 وحدة غاز، وضغطة SHA-256 ما قدره 64.

يكبّر الجدول حجم الإثبات، وتضع السلسلة 20 سقفًا لإثبات الاستدعاء قدره 4,194,304 بايت (4 MiB، كما في السلاسل من 17 إلى 19؛ وكان السقف 8 MiB في السلاسل من 13 إلى 16). وعلى السلسلة 18 بلغ استدعاء من المستوى 12 مع جدول Keccak 3.35 MB، أي معظم السقف، وبلغ استدعاء من المستوى 14 مع جدول SHA-256 ما قدره 1.91 MB، مقابل 1.50 MB بلا أيٍّ منهما (برامج أمثلة). ويُحتسب كل KiB بسعر البايت. أما على السلسلة 15 فلم تُحتسب إلا البايتات التي تتجاوز 2 MiB + 18,432 المجانية، بسعر 1,000 وحدة لكل KiB (الرسوم). استخدم Poseidon2 ما لم يكن لا بد من مطابقة تجزئة خارجية.

الذاكرة وزمن الإثبات

يتزايد زمن الإثبات وذاكرته بحدّة مع المستوى. يُثبَت استدعاء من المستوى 10 في ثوانٍ على حاسوب محمول. وعلى السلسلة 20 يحمل كل نشر وكل استدعاء أيضًا إثباتين من جهة الحزمة: تفويض إنفاق من المستوى 10 (ثوانٍ) وحزمة المستوى 14 التي تدفع الرسم (نحو دقيقتين على حاسوب محمول). استغرقت الاستدعاءات الأربعة التي قِيست على السلسلة 18 ما بين ثلاث دقائق وخمس دقائق ونصف لكل منها، وبلغت ذروة الذاكرة بين 6.1 و6.6 GB (برامج أمثلة). وعلى السلسلة 15 كانت الحزمة إثباتًا واحدًا من المستوى 14 بنحو 100 ث ونحو 5.7 GB. وتجد الأرقام المقيسة في عتاد المدقِّقين والمجمِّعين.

8. النشر

الشبكة التجريبية testnet هي السلسلة 20. يحتاج النشر إلى محفظة rand من fullnode v0.6.8 أو أحدث، ومفتاح، وبعض RAND (المقدمة؛ فالصنبور faucet في السلسلة 20، كما في السلسلة 19، لا يسك إلا للمحافظ الست عشرة التي تدرجها genesis في قائمتها المسموح بها). لا تستطيع محفظة v0.6.7 الإرسال على السلسلة 20: فتكوين genesis في السلسلة 20 يضبط binding_domain: 1، فيشمل كل ربط إثبات وكل رسالة موقّعة تجزئة genesis، وهو ما لا تضعه محفظة v0.6.7. ويكفي نقطة النهاية العامة https://rpc.randprotocol.org لمزامنة المحفظة والنشر والاستدعاء (الإعداد). يُنشر المزاد مع مقطعه العام:

rand fee deploy 258 --public-words 34                 # 0.001 + 0.0001 × (258 + 34) = 0.0302 RAND
rand program deploy auction.bin --public auction-public.txt

يطبع program deploy معرّف البرنامج وhc وعدد الكلمات وعدد كلمات المدخل العام وملخّصه. ويفحص البرنامج مقابل سقف السلسلة، والمدخل العام مقابل max_program_public_words (32,768 على السلسلة 20) قبل أن يُنشئ أي إثبات. ثم يُثبِت حزمة تحويل ذاتي بقيمة صفر تدفع الرسم، ويرسلها، وينتظر تثبيتها. وعلى السلسلة 20، كما منذ السلسلة 17، يعني ذلك إثباتين، لكل منهما سطر خرجه: تفويض إنفاق من المستوى 10 (ثوانٍ، نحو 1.37 MB) وحزمة المستوى 14 (نحو دقيقتين، نحو 1.5 MB). وعلى السلسلة 15 كان إثباتًا واحدًا من المستوى 14 بنحو 100 ث ونحو 1.42 MB. ولا يتضمن رسم النشر حدًّا للغاز. لم يُنشر المزاد على السلاسل من 18 إلى 20؛ ويعرض تصويت مجهول نشرًا بمقطع عام على السلسلة 18 كما طبعته المحفظة.

يتغير معرّف البرنامج مع --public. طبع rand-guest القيمة 148ab0c4…2c73 للشيفرة وحدها. أما النشر مع مقطع عام فيُخزَّن تحت blake3("rand-program-2", base_pc ‖ len ‖ words ‖ len ‖ public)، وهو يربط الاحتياطي reserve والالتزامات الأربعة أيضًا. استخدم المعرّف الذي يطبعه program deploy. فمزاد ثانٍ بالشيفرة نفسها والتزامات مختلفة هو معرّف برنامج مختلف.

rand program show <auction id>        # base_pc, words_len, code_hash, deployed_at, public_words_len, public_digest

code_hash هو hc الذي طبعه rand-guest، مكتوبًا بصورة مختلفة. يطبع rand-guest كلمات الملخّص الثماني كلًّا منها big-endian، بينما تطبع المحفظة وrand program show وrand_getProgram بايتات كل كلمة little-endian، فتنعكس البايتات في كل مجموعة من ثمانية أرقام ست عشرية. وفي البناءين أعلاه يظهر aa04e4c4…050d (Rust) في صورة c4e404aa…4f16، و68605aa8…bea0 (C) في صورة a85a6068…b69a.

9. الاستدعاء

يستدعي البائع بالكلمات الخاصة العشرين كلها:

rand call <auction id> --expect-public auction-public.txt \
  --input 400 --input 2654435769 --input 2135587861 --input 4087136352 --input 1559071514 \
  --input 250 --input 461845907  --input 3432918353 --input 3864292196 --input 2246822507 \
  --input 900 --input 3266489909 --input 668265263  --input 374761393  --input 3550635116 \
  --input 300 --input 4252018373 --input 3042594569 --input 1759714724 --input 625341585

تجلب المحفظة شيفرة البرنامج ومقطعه العام وتتحقق من أن تجزئتهما تطابق المعرّف. ويرفض --expect-public كذلك، قبل الإثبات، إذا لم يكن المقطع العام هو كلمات ذلك الملف. ثم تشغّل المحفظة الاستدعاء في المحاكي وتطبع الغاز بدقة، والحد الذي ستعلنه، والمستوى (أصغر مستوى يلائمه التشغيل، وهو المستوى 10 هنا)، والرسم:

gas: <exact> (declaring <limit>, tier <t>)
fee <amount> RAND (incl. two price steps of headroom)

وتختم كلمات المدخلات العشرين في مظروف لا يفتحه إلا المستدعي، وتُثبِت التشغيل محليًّا، وتدفع الرسم عبر حزمة، وترسل، وتنتظر، وتطبع الإيصال. وعلى السلسلة 20 يعني ذلك ثلاثة إثباتات: الاستدعاء، وتفويض الإنفاق من المستوى 10، وحزمة المستوى 14.

كم يكلّف الاستدعاء. على السلسلة 15 دفع هذا الاستدعاء 0.001 + call_fee(10) = 0.002 RAND. وعلى السلسلة 20 تدفع المحفظة الحد الأدنى من القسم 7، محسوبًا قبل أن يوجد الإثبات، مع ثلاث قيم افتراضية:

  • الحد المعلن هو الغاز الفعلي مقرَّبًا إلى الأعلى إلى أقرب مضاعف للعدد 2^(tier − 2): 256 عند المستوى 10، و1,024 عند المستوى 12، و4,096 عند المستوى 14. يعلن --gas-limit max السقف الأعلى للمستوى، ويعلن --gas-limit N رقمًا محددًا؛
  • يُسعَّر الإثبات وفق السقف الذي تضعه السلسلة، وهو 4,194,304 بايت، لا وفق حجمه الفعلي، لأن إثبات الاستدعاء على chain 20 مرتبط بمعاملته، ولا بد من تثبيت الرسوم أولًا؛
  • هامش قدره خطوتان سعريتان، أي بعامل 1.125²، لأن الاستدعاء يُدرج بعد التسعير بكتلتين أو ثلاث.

يتجاوز --fee القيمة الافتراضية، ويسأل rand fee call <tier> --gas <limit> --bytes <B> العقدة عن الحد الأدنى بنفسها. لم يُجرَ استدعاء المزاد على chain 18 إلى 20، ولذلك لا يظهر هنا سطر الغاز ولا الرسوم الخاصة به. أما التصويت المجهول فله استدعاء كامل على chain 18 كما طبعته المحفظة، بتكلفة 0.0062 RAND.

يحمل الإيصال المستوى وH_IN وH_PUB والمخرجات [1, 2, 400, 4, 0, 0, 0, 0]: وهي الكلمات نفسها التي طبعها المحاكي، لأن الإثبات هو إثبات لذلك التنفيذ نفسه. أما العطاءات الأربعة والأملاح فليست على السلسلة. ويختم --auditor <rand1…> المدخلات أيضًا لمدقِّق مسمّى، ولا ينشر --no-envelope أي نص تنفيذ على الإطلاق، ولا يمكن التراجع عن ذلك (Programs).

10. التحقق من النتيجة

يستطيع مقدِّم العطاء، أو أي شخص آخر، التحقق من المزاد دون الوثوق بالبائع:

  1. الشيفرة. أعد بناء المصدر المنشور باستخدام rand-guest build عند الإيداع نفسه للدوائر، مع Rust 1.98.1 (أو clang 23.1.1). قارن hc بقيمة code_hash من rand program show أو rand_getProgram، مع مراعاة كتابتها بترتيب البايتات المعكوس (القسم 8). البناء قابل لإعادة الإنتاج، فيتطابق الناتجان على أي جهاز.
  2. المقطع العام. اجلبه باستخدام rand_getProgramPublic، أو شغّل rand call --expect-public. تحقق من أن التزامك أنت موجود عند الفهرس الخاص بك.
  3. الإيصال. rand receipt <tx hash> أو rand_getReceipt: الحالة 1، والفائز والسعر، والخانة 7 ليست 0xdeadbeef، وprogram يساوي معرّف المزاد.
  4. المدخلات، إن كان يحق لك الاطلاع عليها. يفتح الأمر rand open-call <tx hash> لدى البائع (أو --as-auditor لدى المدقِّق) المدخلات المختومة، ويتحقق منها مقابل H_IN، ويعيد تشغيل البرنامج، ثم ينتهي برمز غير صفري عند أي عدم تطابق. استخدم محفظة v0.6.8: فمنذ v0.6.8 تُجرى إعادة التشغيل على المقطع العام للبرنامج وعلى ربط الاستدعاء، فيعمل الأمر مع المزاد (fullnode issue #116). وفي v0.6.7 كان open-call يتوقف مع re-running the program on these inputs failed: PublicIndex(0) لأي برنامج نُشر ومعه مقطع عام.

يضمن الإثبات أن القاعدة طُبّقت على فتحات العطاءات الملتزَم بها. لكنه لا ينقل الأموال. يدفع الفائز للبائع بأمر rand send عادي، والإيصال هو ما يستند إليه الطرفان.

استكشاف الأخطاء وإصلاحها

symptomcause and fix
يفشل البناء عند استدعاء poseidon2(…) أو keccak(…) أو sha256_compress(…) كُتب لإيداع أقدمعند aeacf31 تكون poseidon2 دالة unsafe fn، وتأخذ الدالتان الأخريان &mut [u32; 50] و&mut [u32; 24] (القسمان 3 و7)
يبلغ check عن Syscall مع a7 = 7يستدعي البرنامج الضيف poseidon2_len؛ ولا يدرج rand-guest check عند aeacf31 استدعاء النظام 7 (zkp-circuits issue #1)
يبلغ check عن Csr عند 0xc0001073شيفرة المكتبة صُرفت إلى unimp؛ جد الدالة باستخدام llvm-objdump واستبدل الاستدعاء (القسم 6)
يقول run إن التشغيل لا يلائم أي مستوى، أو يحتاج إلى أكثر من المستوى 14صفوف أو تباديل Poseidon2 أكثر مما ينبغي؛ قلّص البرنامج، أو قلّل التجزئة، أو قسّم العمل على عدة استدعاءات مع تمرير ملخص بينها
الخانة 7 = 3735928559انهار البرنامج الضيف المكتوب بلغة Rust (panic)؛ المخرجات التي سبقته غير موثوقة
trap: DoubleWrite(n)كُتبت خانة المخرجات n مرتين؛ مرّر كل مخرج عبر دالة واحدة
trap: InputIndex(i)قرأ البرنامج مدخلًا خاصًا لم يوفره المستدعي؛ اقرأ العدد وتحقق منه أولًا
trap: … عند تحميل أو تخزينعنوان غير محاذى أو خارج النطاق؛ وفي C يكون فهرس مصفوفة تجاوز حدها
invalid value '0x…' for '--input'كلمات المدخلات عشرية في سطر الأوامر
رفض استدعاء مع CallTierTooHighالإثبات أعلى من المستوى 14؛ تحقق بالأمر rand-guest run --tier 14 قبل الاستدعاء
يختلف hc المعاد بناؤه عن المنشورإيداع دوائر أو سلسلة أدوات أو إصدارات تبعيات مختلفة؛ ثبّت الثلاثة والتزم بـ Cargo.lock. وإن اختلفت الكتابة فقط، فكل مجموعة من ثمانية أرقام ست عشرية معكوسة البايتات (القسم 8)
يتوقف rand open-call مع PublicIndex(0)للبرنامج مقطع عام والمحفظة v0.6.7، التي لا تمرر إعادة تشغيلها أي مقطع (fullnode issue #116)؛ استخدم v0.6.8
… is not inside a circuits checkoutانقل دليل البرنامج الضيف إلى داخل النسخة المسحوبة (Deploy a Rust program)

آخر تحقق مقابل الإيداع c0878c3f في fullnode (السلسلة 21) بتاريخ 2026-10-12، وcircuits `7ef3220` (`guest-sdk/src/lib.rs`, `rand-guest/guest.h`, `rand-guest/rt.c`, `rand-guest/README.md`, `research/docs/01-isa.md`, `research/src/machine.rs`); every `rand-guest` and `cargo test` output on this page was produced on 2026-09-25, fullnode `origin/main` `089bdd6` (`docs/cli.md`; `docs/confidential.md` and `executor.rs` for v0.5.6's call-proof caps, commit `1588be9`), circuits `aeacf31` (`guest-sdk/src/lib.rs`, `rand-guest/src/check.rs`, `rand-guest/guest.h`; `git diff 7ef3220 aeacf31 -- guest-sdk rand-guest`), read on 2026-09-30; no output on this page was re-run at that commit, fullnode `90e7716` (`docs/cli.md`, `docs/fees.md` §1.1, `docs/confidential.md`, `docs/guests.md` §7.1, `deploy/genesis-chain18.json`) and the wallet at tag v0.6.7 `86941a1` (`crates/randprotocol-client/src/main.rs`, `wallet.rs`; `gas.rs`, `ledger/mod.rs`, `executor.rs`), read on 2026-09-30; the auction was not deployed or called on chain 18, fullnode tag v0.6.8 `c9c9bd3c` (`docs/program-state.md`, `crates/randprotocol-core/src/ledger/program_state.rs`, the client's `open-call` (#116)) and `rand_getLimits` on chain 20, read on 2026-10-01; the auction was not deployed or called on chain 20, fullnode tag v0.8.0 `c0878c3f` (`CHANGELOG.md`, `docs/fees.md` §1.3) and `rand_getLimits` on chain 21, read on 2026-10-12; the limits this page quotes are chain 21's where they differ from chain 20's; nothing on this page was deployed or called on chain 21. إذا اختلف الكود عن هذه الصفحة فالكود هو الصحيح؛ أبلغونا بذلك.