השפה של GCULpy

‫GCULpy הוא תת-קבוצה של Python עם טיפוסים סטטיים מחמירים, שנועד להיות בטוח, קריא וניתן לביקורת. העיצוב שלה מגביל בכוונה תכונות דינמיות מסוימות של Python כדי למנוע נקודות חולשה נפוצות בחוזים חכמים, ולוודא שההתנהגות של החוזה תמיד צפויה.

בדף הזה מפורטות הנחיות למפרט השפה של GCULpy, כולל מושגי הליבה ומחזור החיים של חוזה ברשת Universal Ledger.

מושגי ליבה

החוזה הבא מגדיר אסימון ERC20 לדוגמה ב-GCULpy:

import gcul

class ERC20Token(gcul.Contract):
    """Sample ERC20 implementation for the Universal Ledger."""

    symbol: str
    total_supply: int
    balance: gcul.AccountField[int]

    def __init__(self, symbol: str):
        self.symbol = symbol
        self.total_supply = 0

    def mint(self, beneficiary: gcul.Account, value: int) -> int:
        """Mints tokens to the given beneficiary."""
        assert self.is_owner(gcul.sender), "Only the owner can mint"
        assert value >= 0, "Mint amount must be non-negative"
        self.total_supply += value
        self.balance[beneficiary] += value
        return value

    def transfer(self, beneficiary: gcul.Account, value: int) -> int:
        """Transfers tokens from the sender to the given beneficiary."""
        assert value >= 0, "Transfer amount must be non-negative"
        assert (
            value <= self.balance[gcul.sender]
        ), "Sender does not have enough balance"
        self.balance[gcul.sender] -= value
        self.balance[beneficiary] += value
        return value

חוזה GCULpy הוא מחלקה שיורשת מ-gcul.Contract. הוא מכיל שדות (שמירת מצב) ושיטות (לוגיקת עיבוד שפועלת בשדות).

שדות

המצב בחוזה GCULpy מאוחסן בשדות. צריך להצהיר על כל השדות עם סוג סטטי ברמת הכיתה. יש שני סוגים של שדות:

  • שדות החוזה מכילים ערך יחיד שמאוחסן בחוזה עצמו. בדוגמה של ERC20Token, השדות symbol: str ו-total_supply: int הם שדות חוזה.

  • שדות החשבון מכילים ערך נפרד לכל חשבון שיוצר אינטראקציה עם החוזה. הם מוצהרים באמצעות gcul.AccountField[T], כאשר T הוא סוג הערך לכל חשבון. בדוגמה של ERC20Token, השדה balance: gcul.AccountField[int] הוא שדה של חשבון. כדי שחוזה יוכל לכתוב לנפח האחסון ששמור לחשבון, צריך להעניק לחוזה הרשאת אחסון באופן מפורש. אחרי שהנתונים נשמרים, רק המופע של החוזה יכול לשנות אותם, ולא החשבון.

הגבלה בשדות יש את מגבלות האחסון הבאות. עסקה שחורגת מאחת מהמגבלות האלה תיכשל.

  • כל השדות של חוזה חולקים רשומת אחסון אחת, שיכולה להכיל כ-5MiB של נתונים שעברו סריאליזציה.
  • כל השדות של החשבון שחוזה מאחסן לכל חשבון בנפרד יכולים להכיל לכל היותר 1MiB של נתונים שעברו סריאליזציה.

Methods

השיטות מגדירות את הלוגיקה של החוזה שניתן להפעיל. הן מתנהגות כמו שיטות של Python ויכולות לקרוא או לשנות את השדות של החוזה.

  • ‫__init__: הפונקציה constructor מופעלת רק פעם אחת, כשהחוזה נפרס בפעם הראשונה. צריך להקצות ערך לכל שדה חוזה ב-constructor. ה-constructor לא מאתחל את שדות החשבון. הערך של כל חשבון נוצר כשהחשבון הזה מעניק את הרשאת האחסון.

  • שיטות פרטיות: שיטות שמתחילות בקו תחתון (לדוגמה, _internal_logic) הן פרטיות, ורק שיטות אחרות באותו חוזה יכולות לקרוא להן. המתורגמן של Universal Ledger אוכף את האילוץ הזה.

  • שיטות ציבוריות: כל שיטה שלא מתחילה בקו תחתון (_) היא ציבורית. כל משתמש עם ההרשאה ROLE_CONTRACT_PARTICIPANT יכול לקרוא לשיטות ציבוריות על ידי שליחת טרנזקציה מסוג InvokeContractMethod.

הגבלה שיטות ציבוריות וכל השיטות של @gcul.readonly לא יכולות להשתמש בסוגים ניתנים לשינוי בחתימות שלהן, למשל list או dict. במקום זאת, השתמשו בהצהרות מסוג לקריאה בלבד כמו Sequence או Mapping.

הגבלה במהלך ההפעלה, השיטות מוגבלות לשימוש בחישובים עד לגבול העליון של הרשת. אם חורגים מהמגבלה הזו, העסקה נכשלת.

מחזור החיים של החוזה

בקטעים הבאים מפורטות הפעולות הנפוצות שקשורות למחזור החיים של חוזה GCULpy.

פריסת חוזה

קודם כול, אתם מקמפלים את קוד המקור של GCULpy באמצעות הקומפיילר gculpyc. לאחר מכן, משתמש עם ROLE_CONTRACT_CREATOR יכול לשלוח טרנזקציית CreateContract כדי לפרוס את קוד הבייט המקומפל ברשת Universal Ledger. הוראות מפורטות מופיעות במדריך פריסת חוזה שניתן לתכנות.

עסקה כזו תיראה כך:

client_transaction {
  sender_id: "OWNER_ACCOUNT_ID"
  create_contract_transaction {
    contract_bytes: "COMPILED_BYTECODE"
    init_arguments {
      key: "symbol"
      value: { string_value: "US02079K1079" }
    }
  }
}

כשהרשת מעבדת את העסקה הזו:

  • ה-constructor, כלומר השיטה __init__, מופעלת עם הארגומנטים שסופקו כדי ליצור מופע חוזה חדש.
  • השולח של העסקה הופך לבעלים של החוזה.
  • מופע החוזה מאוחסן באופן קבוע בספר החשבונות ומוקצה לו מזהה חוזה ייחודי, שמוחזר כחלק מהפלט של העסקה.

מתן הרשאות

כדי שחוזה יוכל לאחסן נתונים בשדה חשבון בשם חשבון, בין אם מדובר בחשבון של משתמש או בחוזה אחר, צריך להעניק הרשאת אחסון לחוזה שמאחסן את הנתונים. זהו שלב אבטחה קריטי.

מתן הרשאות לחשבון משתמש

משתמש עם ROLE_CONTRACT_PARTICIPANT יכול לשלוח טרנזקציית GrantContractPermissions למזהה חוזה ספציפי:

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  grant_contract_permissions_transaction {
    contract_id: "CONTRACT_ID"
    permissions: CONTRACT_PERMISSION_STORAGE
  }
}

כשהרשת מעבדת את העסקה הזו:

  • אם החוזה לא מגדיר שדות חשבון, העסקה נדחית.
  • אם החוזה מגדיר שדות חשבון, כולם מאוכלסים בערכי ברירת מחדל (לדוגמה, contract.balance[gcul.sender] = 0). לאחר מכן הערכים האלה מאוחסנים במצב העולם כחלק מנתוני החשבון.

מתן הרשאות לחוזה של נציג

משתמש עם ROLE_CONTRACT_PARTICIPANT יכול לשלוח טרנזקציית GrantContractPermissions כדי לאפשר לחוזה אחד לאחסן נתונים בשם חוזה אחר שבבעלותו:

client_transaction {
  sender_id: "OWNER_ACCOUNT_ID"
  grant_contract_permissions_transaction {
    contract_id: "TARGET_CONTRACT_ID"
    delegate_contract_id: "DELEGATE_CONTRACT_ID"
    permissions: CONTRACT_PERMISSION_STORAGE
  }
}

כשהרשת מעבדת את העסקה הזו:

  • שולח העסקה, כלומר sender_id, חייב להיות הבעלים של החוזה שמזוהה על ידי delegate_contract_id.
  • חוזה לא יכול להעניק הרשאות אחסון לעצמו, ולכן delegate_contract_id חייב להיות שונה מ-contract_id.
  • הרשאות האחסון ניתנות רק לחוזה שצוין ב-delegate_contract_id, ולא לשולח העסקה.
  • שדות החשבון שמוגדרים בחוזה היעד, contract_id, מאותחלים בשביל החוזה המיופה, delegate_contract_id, וכך שיטות בחוזה היעד יכולות לגשת בבטחה לשדות החשבון שמוגדרים באמצעות gcul.caller או gcul.sender במהלך קריאות בין חוזים.

הפעלת שיטות חוזה

אחרי שפורסים חוזה ונותנים את ההרשאות הנדרשות, המשתמשים יכולים לבצע אינטראקציה עם החוזה על ידי קריאה ל-methods הציבוריות שלו. משתמש עם ROLE_CONTRACT_PARTICIPANT יכול לשלוח טרנזקציית InvokeContractMethod, ולציין את מזהה החוזה, שם השיטה וערכי הארגומנטים של השיטה.

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  invoke_contract_method_transaction {
    contract_id: "CONTRACT_ID"
    method_name: "mint"
    method_arguments {
      key: "beneficiary"
      value: { account_id: "BENEFICIARY_ID" }
    }
    method_arguments {
      key: "value"
      value: { int64_value: 10 }
    }
  }
}

כשהרשת מעבדת את העסקה הזו:

  • מופע החוזה שמשויך ל-CONTRACT_ID שצוין מאוחזר.
  • השיטה mint(beneficiary=Account("BENEFICIARY_ID"), value=10) מופעלת. האובייקט Account של המוטב נוצר ומאומת על ידי סביבת זמן הריצה. הלוגיקה של ה-method יכולה להניח שהמזהה שסופק תקין ומתייחס לחשבון קיים בספר החשבונות.
  • אם השיטה נכשלת מסיבה כלשהי, העסקה תיכשל ולא יבוצעו עדכונים במצב החוזה.
  • אם השיטה מצליחה, המצב המעודכן של החוזה מתועד במצב הכללי.

מפרט השפה

‫GCULpy מיועדת לבטיחות ולחיזוי, ולכן היא לא מאפשרת כמה תכונות של Python. התכונות האלה יסומנו בתווית Restriction. המגבלות האלה נועדו להיות תכונות קבועות של השפה, והן נוספו כדי שיהיה קל יותר לקרוא, לבדוק ולנתח באופן סטטי את הלוגיקה של החוזה, וכדי להגביל התנהגויות מפתיעות או לא בטוחות.

תכונות אחרות מסומנות בתווית Roadmap. הן נמצאות במפת הדרכים להטמעה, אבל הקומפיילר של gculpyc עדיין לא תומך בהן.

סוגים

‫GCULpy תומכת במגוון סוגי משתנים נפוצים, עם דגש חזק על הקלדה סטטית.

סוגי ערכים עיקריים:

  • כבר יש תמיכה ב-int, bool, str, None.
  • מפת דרכים Decimal, bytes, Enum נמצאים במפת הדרכים.
  • ההגבלה float ו-complex לא מותרת.
  • ערכי Restriction int הם מספרים שלמים חתומים בטווח 64 ביט, מ-‎-263 עד ‎263 - 1. ליטרל של מספר שלם מחוץ לטווח הזה יגרום לשגיאה בזמן הקימפול, ופעולה שהתוצאה שלה מחוץ לטווח הזה תגרום לשגיאת זמן ריצה.
  • הגבלה הערכים של str יכולים להיות באורך של עד 1,000,000 תווים. מחרוזת ארוכה יותר תגרום לשגיאה בזמן ההידור. אם תעבירו מחרוזת ארוכה יותר כארגומנט של method, או אם תיצרו מחרוזת כזו כתוצאה של פעולה, תתקבל שגיאת זמן ריצה.

סוגי המאגרים:

  • ‫list וגם dict:
    • ‫list[T]: רצף שניתן לשינוי. תומך בהוספה לאינדקס, לדוגמה my_list[0], ובשיטה append().
    • ‫dict[K, V]: מיפוי שניתן לשינוי. תומך בהוספה לאינדקס, לדוגמה my_dict[key], ובשיטה pop().
  • ‫collections.abc.Sequence וגם collections.abc.Mapping:
    • ‫collections.abc.Sequence[T]: הפניה לקריאה בלבד לרצף. תומך בקריאת רכיבים לפי אינדקס, לדוגמה my_seq[0], אבל אי אפשר לשנות או למחוק רכיבים. ‫list[T] הוא סוג משנה של Sequence[T], כך שאפשר להעביר, להחזיר או להקצות ערך list[T] או ליטרל של רשימה ליעד שהוקלד כ-Sequence[T], אבל אי אפשר להקצות Sequence[T] ל-list[T].
    • ‫collections.abc.Mapping[K, V]: הפניה לקריאה בלבד למיפוי. אפשר לקרוא רשומות לפי מפתח, למשל my_map[key], אבל אי אפשר לשנות או למחוק רשומות. ‫dict[K, V] הוא סוג משנה של Mapping[K, V], כך שאפשר להעביר, להחזיר או להקצות ערך dict[K, V] או מילון מילולי ליעד שהוקלד כ-Mapping[K, V], אבל אי אפשר להקצות Mapping[K, V] ל-dict[K, V].
  • מפת דרכים tuple, set, dataclass נמצאים במפת הדרכים.
  • הגבלה צריך לציין סוגים קונקרטיים לערכים במאגר, לדוגמה dict[str, int] מותר, אבל dict או dict[str, Any] פשוטים אסורים.
  • מפת דרכים חיתוך רשימה, לדוגמה my_list[1:3].
  • מפת דרכים שיטות אחרות, כמו .get(),‏ .keys(),‏ .values(),‏ .items(),‏ .clear() או .extend().

מגבלה כל המשתנים – כולל שדות של חוזים וחשבונות, פרמטרים של פונקציות וסוגי החזרה – חייבים להיות מוגדרים ומוקלדים באופן סטטי. אי אפשר לשנות את הסוג שלהם בזמן הריצה, ורק סוגים קונקרטיים נתמכים. אי אפשר להשתמש בסוגים כערכים, למשל אי אפשר לאחסן אותם במשתנים או להעביר אותם לפונקציות כארגומנטים. ניסיון להקצות ערך לשדה שלא הוגדר יגרום לשגיאה בזמן הקימפול.

מגבלה צריך להקצות משתנה מקומי לפני שקוראים אותו. למשתנים מקומיים אין ערך ברירת מחדל, ולכן קריאה של משתנה לפני שהוא מוקצה גורמת לשגיאת זמן ריצה.

מחלקות וירושה

אפשר להגדיר רק מחלקות שהן מחלקות משנה ישירות של gcul.Contract או של gcul.ContractInterface. הכלל המחמיר הזה מונע את המורכבות של ירושה מלאה ב-Python, שעלולה לגרום לבאגים שקשה למצוא ולהקשות על הבנת הקוד. מטעמי אבטחה, ניסיון לשנות מאפיין או שיטה ממחלקת הורה יגרום לשגיאה, כדי להגן מפני התנהגות לא צפויה.

מפת דרכים: GCULpy תציע גמישות רבה יותר תוך שמירה על עקרונות הליבה שלה. מפת הדרכים כוללת תמיכה בירושה יחידה במחלקות שמוגדרות על ידי המשתמש, עם שיטה לביטול מפורש שמנוהלת באמצעות מעצב @override. בנוסף, הפונקציה המובנית super() תהיה נתמכת רק בצורה ללא ארגומנטים, כדי להבטיח פעולות ישירות וצפויות.

המודול gcul

‫GCULpy מספק מודול gcul מובנה עם סוגים, מעצבים ומשתנים חיוניים לפיתוח חוזים.

כיתה gcul.Contract

המחלקה הבסיסית לכל החוזים. אי אפשר ליצור מופעים של חוזה באופן ישיר. חוזים נוצרים רק באמצעות טרנזקציות של CreateContract. אי אפשר לשנות שיטות ומאפיינים מהמחלקה הבסיסית gcul.Contract במחלקות משנה.

  • Contract.is_owner(account: Account) -> bool

    הפונקציה מחזירה את הערך True אם החשבון שצוין הוא הבעלים של החוזה.

  • Contract.call(target: ContractInterface, payment: Amount | None = None) -> ContractMethods

    מתחילה אינטראקציה בין חוזים עם target. במקום להפעיל שיטות ישירות ביעד (למשל, target.foo()), צריך להשתמש ב-self.call() כדי לנתב באופן מפורש את ההפעלה דרך זמן הריצה של ה-ledger. המנגנון הזה מחזיר אובייקט proxy מאובטח שמשמש להפעלת שיטות ציבוריות תואמות בחוזה היעד, כדי לוודא שגבולות הביצוע נאכפים בקפדנות וכל payment מועבר בצורה בטוחה. פרטים נוספים זמינים במאמר ContractInterface.

    צריך לספק payment כשמפעילים method שמעוטר ב-@gcul.payable, ולהשמיט אותו ב-methods שלא ניתנים לתשלום.

    החוזה target חייב להיות שונה. קריאה לחוזה הנוכחי דרך self.call() גורמת לשגיאת זמן ריצה. כדי להפעיל שיטה אחרת באותו חוזה, מפעילים אותה ישירות, לדוגמה, self.foo().

  • Contract.native_transfer(beneficiary: Account, amount: Amount) -> None

    העברה של סכום בתוך החוזה ישירות מהיתרה של החוזה אל beneficiary.

  • Contract.id: str

    מזהה מחרוזת ייחודי של החוזה.

כיתה gcul.Account

סוג מובנה שמייצג חשבון של משתמש או של חוזה בספר החשבונות. אי אפשר ליצור אובייקטים מסוג gcul.Account באופן ישיר. סביבת זמן הריצה יוצרת אותם בשבילכם ומספקת אותם כארגומנטים של פונקציות או שיטות. כדי להעביר חשבון כארגומנט של עסקה, צריך לספק AccountValue שמכיל את מזהה החשבון. סביבת זמן הריצה מאמתת את המזהה וממירה אותו לאובייקט חשבון מלא. אם המזהה לא שייך לחשבון רשום, העסקה נכשלת. כך תוכלו לוודא שאתם עובדים רק עם חשבונות תקינים.

ההגדרה של המחלקה מקבילה בערך ל:

@dataclasses.dataclass(frozen=True)
class Account:
  """A valid account on the ledger."""

  id: str  # The ID of the account as a string.
  • Account.id: str

    מזהה מחרוזת ייחודי של החשבון.

כיתה gcul.Amount

מייצג ערך כספי שנקוב במטבע ספציפי בספר החשבונות. יוצרים מופע של סכום באמצעות gcul.Amount(value: int, currency: gcul.Currency). ההגדרה של המחלקה שווה בערך ל:

@dataclasses.dataclass(frozen=True)
class Amount:
  """Represents an amount denominated in a ledger currency."""

  value: int
  currency: Currency

כיתה gcul.Currency

מייצג מטבע שמוגדר במלואו בספר החשבונות. ההגדרה של המחלקה שווה בערך ל:

@dataclasses.dataclass(frozen=True)
class Currency:
  """Represents a fully qualified currency on the ledger."""

  code: str
  operator_id: str

כיתה gcul.ContractInterface

המחלקה הבסיסית להצהרה על הממשק הציבורי של חוזה. אי אפשר לשנות שיטות ומאפיינים מהמחלקה הבסיסית gcul.ContractInterface במחלקות משנה. בנוסף, השיטות בהצהרת הממשק לא יכולות לכלול קוד, אלא רק את ....

כדי שחוזה יפעיל שיטות בחוזה אחר שנפרס ב-ledger, צריך להצהיר על מחלקת ממשק שעוברת בירושה מ-gcul.ContractInterface. ה-stubs של השיטות חייבים להתאים להצהרה שלהם בחוזה היעד. זה כולל ארגומנטים, סוג החזרה והערות של מעצבים כמו @gcul.readonly או @gcul.payable.

בתוך קוד החוזה, משתמשים ב-self.call(target, payment) כדי להפעיל שיטה מחוזה אחר.

הדוגמה הבאה ממחישה איך חוזה TokenVendor יוצר אינטראקציה עם חוזה אחר שמטמיע את ERC20Interface. הוא משתמש ב-self.call() כדי להעביר אסימונים לקונה בתמורה לתשלום במטבע שרשום בספר החשבונות:

class ERC20Interface(gcul.ContractInterface):
  """Interface for an external ERC20 token contract."""

  def transfer(self, beneficiary: gcul.Account, value: int) -> int:
    ...

class TokenVendor(gcul.Contract):
  """A vendor contract that distributes ERC20 tokens."""

  price_per_token: int

  def __init__(self, price_per_token: int) -> None:
    self.price_per_token = price_per_token

  @gcul.payable
  def purchase_tokens(self, token: ERC20Interface, quantity: int) -> int:
    """Accepts on-ledger currency for a specific quantity of ERC20 tokens."""

    # Enforce positive quantity.
    assert quantity > 0, "Quantity must be positive"

    # Calculate the required payment.
    required_payment = quantity * self.price_per_token
    assert gcul.payment.value == required_payment, "Incorrect payment amount"

    # Invokes `transfer` on the external ERC20 token contract
    # to move tokens from the vendor's own balance to the caller.
    return self.call(token).transfer(gcul.caller, quantity)

אפשר להשתמש בסוגי ממשקים גם כשדות חוזה, ולא רק כפרמטרים של שיטות. אחסון של ממשק בשדה מאפשר לחוזה להחזיק הפניה לחוזה אחר בין עסקאות, ולהחליף את ההפניה הזו מאוחר יותר. בחוזה לדוגמה הבא נעשה שימוש ב-ERC20Interface שהוגדר קודם:

class Router(gcul.Contract):
  """Holds a replaceable reference to an ERC20 token contract."""

  target: ERC20Interface

  def __init__(self, target: ERC20Interface) -> None:
    self.target = target

  def set_target(self, new_target: ERC20Interface) -> None:
    assert self.is_owner(gcul.sender), "Only the owner can set the target"
    self.target = new_target

כיתה gcul.AccountField

סוג גנרי מובנה להצהרה על שדות בחשבון. gcul.AccountField[T] מכיל ערך נפרד מסוג T לכל חשבון שנתן לחוזה הרשאת אחסון. הוא מתנהג כמו מיפוי עם מפתחות gcul.Account, שבהם אפשר להשתמש כדי לקרוא או לכתוב ערך לכל חשבון ספציפי. לדוגמה:

class Token(gcul.Contract):
  balance: gcul.AccountField[int]

  def deposit(self, account: gcul.Account, value: int) -> None:
    self.balance[account] = value  # Writes the value for one account.

  def get_balance(self, account: gcul.Account) -> int:
    return self.balance[account]  # Reads the value for one account.

השדה T חייב להיות מסוג שיש לו ערך ברירת מחדל, ולכן אי אפשר להשתמש ב-gcul.Account, ב-gcul.Amount, ב-gcul.Currency ובסוגי ממשקים כערכים של שדות בחשבון. כשחשבון מעניק הרשאת אחסון לחוזה, כל שדה בחשבון מאוכלס בערך ברירת המחדל של הסוג שלו באותו חשבון.

הגבלה הפעולה היחידה שנתמכת בשדה חשבון היא הוספה לאינדקס באמצעות חשבון. אי אפשר להקצות את השדה עצמו למשתנה, להעביר אותו כארגומנט, להחזיר אותו מפונקציה או לבצע עליו איטרציה.

הגבלה אי אפשר למחוק את הרשומות בשדות של החשבון. אפשר להחליף ערך שמור, אבל הרשומה עצמה היא קבועה.

הגבלה שדות החשבון של gcul.sender, gcul.caller וכל gcul.Account שמועבר כארגומנט של שיטה (כולל בתוך רשימות או מאגרים אחרים) נטענים לפני הפעלת השיטה. בחשבונות אחרים, טעינה מדורגת של שדות חשבון בעסקה אחת יכולה להתבצע לכל היותר ל-15 זוגות (contract, account) נפרדים. גישה לשדה חשבון של אותו זוג כמה פעמים בעסקה נספרת רק פעם אחת. עסקה שחורגת מהמגבלה הזו תיכשל.

@gcul.readonly

מוסיפה הערה לשיטת חוזה כשיטה לקריאה בלבד. הצהרות על שיטות עם המעצב הזה לא יכולות לשנות את מצב החוזה או לבצע העברות מטבע ב-ledger. הם גם לא יכולים להשתמש בסוגי מאגרים ניתנים לשינוי בחתימות שלהם, למשל list או dict. במקום זאת, הם צריכים להשתמש בהצהרות מסוג לקריאה בלבד, כמו Sequence או Mapping.

@gcul.payable

הוספת הערה לשיטת חוזה שדורשת ביצוע תשלום. אם שיטה מצפה לקבל תשלום במטבע שרשום בספר החשבונות באמצעות gcul.payment, צריך לסמן אותה במעטר הזה, אחרת העסקה תיכשל.

gcul.sender: gcul.Account

מכיל הפניה לחשבון שחתם על העסקה הנוכחית של InvokeContractMethod ושלח אותה. הערך של gcul.sender לא משתנה במהלך ביצוע העסקה, גם אם היא כוללת קריאות בין חוזים, בניגוד ל-gcul.caller שמפנה למבצע הקריאה המיידי.

gcul.caller: gcul.Account

מכיל הפניה למבצע הקריאה המיידי של ה-method הנוכחי. בעסקאות ישירות של משתמשים, הערך של gcul.caller תואם לערך של gcul.sender ומצביע על מי ששלח את העסקה. בקריאות בין חוזים, gcul.caller תואם למזהה החשבון של החוזה שמבצע את הקריאה. קריאות פנימיות בתוך חוזה לא משנות את הערך של gcul.caller.

gcul.payment: gcul.Amount

התשלום מועבר לאמצעי תשלום שמופיע לידו הסמל @gcul.payable. גישה אל gcul.payment בתוך שיטה שלא ניתן לשלם בה גורמת לשגיאה בזמן הידור.

אופרטורים

רוב האופרטורים שזמינים ב-Python נתמכים ב-GCULpy ופועלים כצפוי.

  • חיבור (+) וחיסור (-), כולל צורות אונריות ובינאריות.
  • כפל (*), חילוק עם עיגול כלפי מטה (//) ושארית (%).
  • העלאה בחזקה (**) למעריכים חיוביים.
  • השוואות (<, <=, >, >=, ==, !=).
  • פעולות סיביות: and‏ (&), ‏ or‏ (|), ‏ xor‏ (^), הזזה שמאלה (<<), הזזה ימינה (>>), שלילה (~).
  • פעולות בוליאניות (and, or, not).
  • מפת דרכים: זהות האובייקט (is).
  • הגבלה: אסור להשתמש במעריכים שליליים, והם גורמים לשגיאת זמן ריצה.
  • הגבלה חילוק אמיתי (/), כי סוג ההחזרה שלו הוא float, אסור וגורם לשגיאה בזמן הקימפול.

בקרת זרימה

רוב הצהרות בקרת הזרימה מ-Python פועלות ב-GCULpy עם אותה סמנטיקה:

  • pass דוחות.
  • קריאות פנימיות להפעלת פונקציות (אותו חוזה, לא רקורסיבי).
  • assert דוחות.
  • if ... then .. else ... דוחות.
  • for VAR in CONTAINER דוחות.
  • קריאות לפונקציות חיצוניות (לכל חוזה אחר, לא רקורסיבי).
  • דפי חשבון של break ו-continue.
  • raise דוחות.
  • הצהרות מפת דרכים try ... except.
  • הצהרות מפת דרכים match.
  • הצהרות לגבי מפת הדרכים generators ו-yield.
  • מנהלי הקשר של Roadmap והצהרות with.

הגבלה GCULpy היא בכוונה לא שלמה לפי טיורינג כדי למנוע לולאות אינסופיות ולאפשר ניתוח סטטי. כך זה מתבצע:

  • אין לולאות אינסופיות: מותר לבצע איטרציה רק באמצעות לולאות for על מאגרים סופיים או רצפים מוגבלים עם range(...). אסור להשתמש בלולאות while. חלק מהעדכונים של מאגרים, למשל הוספה או הסרה של רכיבים לרשימה או מפתחות למילון, אסורים בזמן איטרציה עליהם.
  • אין רקורסיה: פונקציה לא יכולה לקרוא לעצמה, באופן ישיר או עקיף. סביבת זמן הריצה מבצעת בדיקות סטטיות ובדיקות בזמן ריצה כדי לזהות ולדחות את השימוש ברקורסיה.
  • אין זרימת בקרה אסינכרונית: כדי לשמור על עקרונות התכנון המרכזיים של יכולת חיזוי, אבטחה והפעלה דטרמיניסטית, אסור להשתמש בפרימיטיבים של async. פעולות אסינכרוניות מקשות על ההבנה של בקרת הזרימה של התוכנית, ולעיתים קרובות מובילות לנקודות חולשה ולמרוצי תהליכים.

פונקציות מובנות

‫GCULpy תומכת בפונקציות המובנות הבאות:

  • abs(x)

    הפונקציה מחזירה את הערך המוחלט של מספר שלם x. הארגומנטים חייבים להיות מסוג int.

  • len(s)

    הפונקציה מחזירה את האורך, כלומר מספר הפריטים, של מחרוזת או של מאגר סופי.

  • max(iterable)
    max(arg1, arg2, *args)

    הפונקציה מחזירה את הפריט הגדול ביותר ב-iterable או את הארגומנט הגדול ביותר מבין שני ארגומנטים או יותר. קריאה ל-max() על איטרבל ריק גורמת לשגיאת זמן ריצה.

  • min(iterable)
    min(arg1, arg2, *args)

    הפונקציה מחזירה את הפריט הקטן ביותר במערך שניתן לעבור עליו בלולאה, או את הערך הקטן ביותר מבין שני ארגומנטים או יותר. קריאה ל-min() על איטרבל ריק גורמת לשגיאת זמן ריצה.

  • range(stop)
    range(start, stop[, step=1])

    יוצרת סדרה חשבונית של מספרים שלמים עם מספר שלם שאינו אפס step.

    הגבלה אפשר להשתמש רק בשורה עם לולאות for, לדוגמה for i in range(10): .... אי אפשר להקצות אובייקטים של טווחים למשתנים, לאחסן אותם בשדות, להעביר אותם כארגומנטים לפונקציות או לשלוח אליהם שאילתות באמצעות len().

מפת דרכים הפונקציות המובנות הבאות נמצאות במפת הדרכים להטמעה:

A
all()
any()

B
bin()
bool()
bytes()

C
chr()

D
dict()
divmod()

E
enumerate()

F
format()
frozenset()

H
hash()
hex()

I
id()
int()

L
list()

O
oct()
ord()

P
pow()
property()

R
repr()
reversed()

S
set()
sorted()
staticmethod()
str()
sum()
super()

T
tuple()

Z
zip()

נתוני גרסה

  • ‫3 בנובמבר 2026. השקנו את גרסת ה-GA של המקמפל gculpyc (v0.2.0), כולל תמיכה בקריאות בין חוזים, העברות מטבע ותשלומים ב-ledger, שיטות @gcul.readonly, ‏list, הצהרות נוספות של זרימת בקרה ופונקציות מובנות.

  • ‫28 בינואר 2026. גרסה מוקדמת של הקומפיילר של gculpyc זמינה למשתתפים בגרסת טרום-השקה פרטית של Universal Ledger. במדריך פריסת חוזה שניתן לתכנות מוסבר איך משתמשים בקומפיילר.