Die GCULpy-Sprache

GCULpy ist eine strikte, statisch typisierte Teilmenge von Python, die sicher, lesbar und prüfbar ist. Das Design schränkt bestimmte dynamische Funktionen von Python absichtlich ein, um häufige Sicherheitslücken bei Smart Contracts zu vermeiden und dafür zu sorgen, dass das Verhalten eines Vertrags immer vorhersehbar ist.

Diese Seite enthält eine Referenz für die GCULpy-Sprachspezifikation, die die Kernkonzepte und den Lebenszyklus eines Vertrags in einem Universal Ledger-Netzwerk abdeckt.

Wichtige Konzepte

Der folgende Vertrag definiert ein Beispiel für ein ERC20-Token in 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

Ein GCULpy-Vertrag ist eine Klasse, die von gcul.Contract erbt. Sie enthält Felder (Speicherstatus) und Methoden (Verarbeitungslogik, die auf Feldern ausgeführt wird).

Felder

Der Status in einem GCULpy-Vertrag wird in Feldern gespeichert. Alle Felder müssen mit einem statischen Typ auf Klassenebene deklariert werden. Es gibt zwei Arten von Feldern:

  • Vertragsfelder enthalten einen einzelnen Wert, der mit dem Vertrag selbst gespeichert wird. Im Beispiel ERC20Token sind symbol: str und total_supply: int Vertragsfelder.

  • Kontofelder enthalten einen separaten Wert für jedes Konto, das mit dem Vertrag interagiert. Sie werden mit gcul.AccountField[T] deklariert, wobei T der Typ des kontospezifischen Werts ist. Im Beispiel ERC20Token ist balance: gcul.AccountField[int] ein Kontofeld. Bevor ein Vertrag in den für dieses Konto reservierten Speicherplatz schreiben kann, muss die Speicherberechtigung explizit dem Vertrag gewährt werden. Sobald die Daten gespeichert sind, können sie nur von der Vertragsinstanz geändert werden, nicht vom Konto.

Für Einschränkungsfelder gelten die folgenden Speicherlimits. Eine Transaktion, die eines der beiden Limits überschreitet, schlägt fehl.

  • Alle Vertragsfelder eines Vertrags teilen sich einen einzigen Speichereintrag, der insgesamt etwa 5 MiB serialisierte Daten enthalten kann.
  • Alle Kontofelder, die ein Vertrag für jedes einzelne Konto speichert, können insgesamt maximal 1 MiB serialisierte Daten enthalten.

Methoden

Methoden definieren die ausführbare Logik eines Vertrags. Sie verhalten sich wie Python-Methoden und können die Felder des Vertrags lesen oder ändern.

  • __init__: Der Konstruktor wird nur einmal aufgerufen, wenn der Vertrag zum ersten Mal bereitgestellt wird. Jedem Vertragsfeld muss im Konstruktor ein Wert zugewiesen werden. Der Konstruktor initialisiert keine Kontofelder. Der Wert jedes Kontos wird erstellt, wenn dieses Konto die Speicherberechtigung gewährt.

  • Private Methoden: Methoden, die mit einem Unterstrich beginnen (z. B. _internal_logic), sind privat und können nur von anderen Methoden innerhalb desselben Vertrags aufgerufen werden. Der Universal Ledger-Interpreter erzwingt diese Einschränkung.

  • Öffentliche Methoden: Jede Methode, die nicht mit einem Unterstrich (_) beginnt, ist öffentlich. Öffentliche Methoden können von jedem Nutzer mit der ROLE_CONTRACT_PARTICIPANT aufgerufen werden, indem eine InvokeContractMethod-Transaktion gesendet wird.

Einschränkung: Öffentliche Methoden und alle @gcul.readonly-Methoden können keine änderbaren Typen in ihren Signaturen verwenden, z. B. list oder dict. Verwenden Sie stattdessen schreibgeschützte Typdeklarationen wie Sequence oder Mapping.

Einschränkung: Während der Ausführung sind Methoden auf eine netzwerkweite Obergrenze für die Rechennutzung beschränkt. Das Überschreiten dieses Limits führt zu einem Transaktionsfehler.

Vertragslebenszyklus

In den folgenden Abschnitten werden die typischen Vorgänge im Lebenszyklus eines GCULpy-Vertrags beschrieben.

Vertrag bereitstellen

Zuerst kompilieren Sie Ihren GCULpy-Quellcode mit dem gculpyc-Compiler. Anschließend kann ein Nutzer mit der Rolle ROLE_CONTRACT_CREATOR eine CreateContract-Transaktion senden, um den kompilierten Bytecode in einem Universal Ledger-Netzwerk bereitzustellen. Eine ausführliche Anleitung finden Sie in der Anleitung Programmierbaren Vertrag bereitstellen.

Eine solche Transaktion würde so aussehen:

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

Wenn das Netzwerk diese Transaktion verarbeitet:

  • Der Konstruktor, also die Methode __init__, wird mit den angegebenen Argumenten ausgeführt, um eine neue Vertragsinstanz zu erstellen.
  • Der Absender der Transaktion wird zum Inhaber des Vertrags.
  • Die Vertragsinstanz wird dauerhaft im Ledger gespeichert und erhält eine eindeutige Vertrags-ID, die als Teil der Transaktionsausgabe zurückgegeben wird.

Berechtigungen erteilen

Bevor ein Vertrag im Namen eines Kontos (entweder eines Nutzerkontos oder eines anderen Vertrags) Daten in einem Kontofeld speichern kann, muss dem Vertrag, der die Daten speichert, die Speicherberechtigung gewährt werden. Dies ist ein wichtiger Sicherheitsschritt.

Berechtigungen für ein Nutzerkonto gewähren

Ein Nutzer mit einer ROLE_CONTRACT_PARTICIPANT kann eine GrantContractPermissions-Transaktion für eine bestimmte Vertrags-ID senden:

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

Wenn das Netzwerk diese Transaktion verarbeitet:

  • Wenn im Vertrag keine Kontofelder definiert sind, wird die Transaktion abgelehnt.
  • Wenn der Vertrag Kontofelder definiert, werden alle mit Standardwerten (z. B. contract.balance[gcul.sender] = 0) gefüllt. Diese Werte werden dann als Teil der Kontodaten im World State gespeichert.

Berechtigungen für einen Delegatvertrag erteilen

Ein Nutzer mit einem ROLE_CONTRACT_PARTICIPANT kann eine GrantContractPermissions-Transaktion senden, um einem Vertrag zu erlauben, Daten im Namen eines anderen Vertrags zu speichern, den er besitzt:

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
  }
}

Wenn das Netzwerk diese Transaktion verarbeitet:

  • Der Absender der Transaktion, also sender_id, muss der Inhaber des durch delegate_contract_id identifizierten Vertrags sein.
  • Ein Vertrag kann sich selbst keine Speicherberechtigungen gewähren. Daher muss sich delegate_contract_id von contract_id unterscheiden.
  • Speicherberechtigungen werden ausschließlich dem in delegate_contract_id angegebenen Vertrag gewährt, nicht dem Absender der Transaktion.
  • Im Zielvertrag contract_id definierte Kontofelder werden für den Delegatvertrag delegate_contract_id initialisiert. Dadurch können Methoden im Zielvertrag bei vertragsübergreifenden Aufrufen sicher auf Kontofelder zugreifen, die mit gcul.caller oder gcul.sender verschlüsselt sind.

Vertragsmethoden aufrufen

Sobald ein Vertrag bereitgestellt und alle erforderlichen Berechtigungen gewährt wurden, können Nutzer mit ihm interagieren, indem sie seine öffentlichen Methoden aufrufen. Ein Nutzer mit einem ROLE_CONTRACT_PARTICIPANT kann eine InvokeContractMethod-Transaktion senden und dabei die Vertrags-ID, den Methodennamen und die Werte der Methodenargumente angeben.

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 }
    }
  }
}

Wenn das Netzwerk diese Transaktion verarbeitet:

  • Die Vertragsinstanz, die mit der angegebenen CONTRACT_ID verknüpft ist, wird abgerufen.
  • Die Methode mint(beneficiary=Account("BENEFICIARY_ID"), value=10) wird ausgeführt. Das Account-Objekt für den Begünstigten wird von der Laufzeit erstellt und validiert. Die Logik der Methode kann davon ausgehen, dass die angegebene ID gültig ist und sich auf ein vorhandenes Konto im Ledger bezieht.
  • Wenn die Methode aus irgendeinem Grund fehlschlägt, schlägt die Transaktion fehl und es werden keine Aktualisierungen am Status des Vertrags vorgenommen.
  • Wenn die Methode erfolgreich ist, wird der aktualisierte Status des Vertrags im World State aufgezeichnet.

Sprachspezifikation

GCULpy ist auf Sicherheit und Vorhersehbarkeit ausgelegt und lässt daher mehrere Python-Funktionen nicht zu. Diese werden mit dem Label Einschränkung gekennzeichnet. Diese Einschränkungen sind als dauerhafte Sprachfunktionen gedacht, die eingeführt wurden, um die Vertragslogik leichter lesbar, prüfbar und statisch analysierbar zu machen und überraschende oder unsichere Verhaltensweisen zu begrenzen.

Andere Funktionen sind mit dem Label Roadmap gekennzeichnet. Diese sind auf der Implementierungs-Roadmap, werden aber noch nicht vom gculpyc-Compiler unterstützt.

Typen

GCULpy unterstützt eine Reihe gängiger Variablentypen, wobei der Schwerpunkt auf statischer Typisierung liegt.

Zentrale Werttypen:

  • int, bool, str und None werden bereits unterstützt.
  • Roadmap: Decimal, bytes und Enum sind auf der Roadmap.
  • Einschränkung: float und complex sind nicht zulässig.
  • Einschränkung: int-Werte sind vorzeichenbehaftete 64-Bit-Ganzzahlen von -263 bis 263 - 1. Ein Ganzzahlliteral außerhalb dieses Bereichs führt zu einem Fehler bei der Kompilierung und ein Vorgang, dessen Ergebnis außerhalb dieses Bereichs liegt, zu einem Laufzeitfehler.
  • Einschränkung: str-Werte dürfen maximal 1.000.000 Zeichen lang sein. Ein längeres Stringliteral führt zu einem Fehler bei der Kompilierung. Wenn Sie einen längeren String als Methodenargument übergeben oder einen längeren String als Ergebnis eines Vorgangs erzeugen, führt dies zu einem Laufzeitfehler.

Containertypen:

  • list und dict:
    • list[T]: Eine änderbare Sequenz. Unterstützt die Indexierung, z. B. my_list[0], und die Methode append().
    • dict[K, V]: Eine veränderliche Zuordnung. Unterstützt die Indexierung, z. B. my_dict[key], und die Methode pop().
  • collections.abc.Sequence und collections.abc.Mapping:
    • collections.abc.Sequence[T]: Ein schreibgeschützter Verweis auf eine Sequenz. Unterstützt das Lesen von Elementen nach Index, z. B. my_seq[0], aber Elemente können nicht geändert oder gelöscht werden. list[T] ist ein Untertyp von Sequence[T]. Sie können also einen list[T]-Wert oder ein Listenliteral an ein Ziel übergeben, zurückgeben oder zuweisen, das als Sequence[T] typisiert ist. Sie können jedoch kein Sequence[T] einem list[T] zuweisen.
    • collections.abc.Mapping[K, V]: Ein schreibgeschützter Verweis auf eine Zuordnung. Unterstützt das Lesen von Einträgen nach Schlüssel, z. B. my_map[key], aber Einträge können nicht geändert oder gelöscht werden. dict[K, V] ist ein Untertyp von Mapping[K, V]. Sie können also einen dict[K, V]-Wert oder ein Wörterbuchliteral an ein Ziel mit dem Typ Mapping[K, V] übergeben, zurückgeben oder zuweisen. Sie können jedoch kein Mapping[K, V] einem dict[K, V] zuweisen.
  • Roadmap: tuple, set und dataclass sind auf der Roadmap.
  • Einschränkung Für Werte in einem Container müssen konkrete Typen angegeben werden. Beispielsweise ist dict[str, int] zulässig, ein einfacher dict oder dict[str, Any] jedoch nicht.
  • Roadmap: Listenslicing, z. B. my_list[1:3].
  • Roadmap: Andere Methoden wie .get(), .keys(), .values(), .items(), .clear() oder .extend().

Einschränkung: Alle Variablen, einschließlich Vertrags- und Kontofelder, Funktionsparameter und Rückgabetypen, müssen statisch definiert und typisiert werden. Ihr Typ kann zur Laufzeit nicht geändert werden und es werden nur konkrete Typen unterstützt. Typen können nicht als Werte verwendet werden. Sie können beispielsweise nicht in Variablen gespeichert oder als Argumente an Funktionen übergeben werden. Der Versuch, einem nicht deklarierten Feld einen Wert zuzuweisen, führt zu einem Kompilierungsfehler.

Einschränkung: Eine lokale Variable muss zugewiesen werden, bevor sie gelesen wird. Lokale Variablen haben keinen Standardwert. Wenn Sie eine Variable lesen, bevor sie zugewiesen wurde, führt dies zu einem Laufzeitfehler.

Klassen und Vererbung

Sie können nur Klassen definieren, die direkte Unterklassen von gcul.Contract oder gcul.ContractInterface sind. Diese strenge Regel verhindert die Komplexität der vollständigen Python-Vererbung, die zu schwer zu findenden Fehlern führen und die Nachvollziehbarkeit des Codes erschweren kann. Aus Sicherheitsgründen wird beim Versuch, ein Attribut oder eine Methode einer übergeordneten Klasse zu überschreiben, ein Fehler ausgegeben. Dies ist eine klare Schutzmaßnahme gegen unerwartetes Verhalten.

Roadmap GCULpy wird mehr Flexibilität bieten, während die Kernprinzipien beibehalten werden. Die Roadmap umfasst die Unterstützung der einfachen Vererbung für benutzerdefinierte Klassen, wobei das Überschreiben von Methoden explizit mit einem @override-Decorator verwaltet wird. Außerdem wird die integrierte Funktion super() nur in ihrer argumentfreien Form unterstützt, um direkte, vorhersehbare Vorgänge zu ermöglichen.

Das gcul-Modul

GCULpy bietet ein integriertes gcul-Modul mit wichtigen Typen, Decorators und Variablen für die Vertragsentwicklung.

Klasse gcul.Contract

Die Basisklasse für alle Verträge. Sie können keine Instanzen direkt erstellen. Verträge werden nur über CreateContract-Transaktionen instanziiert. Methoden und Attribute der Basisklasse gcul.Contract können in Unterklassen nicht überschrieben werden.

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

    Gibt True zurück, wenn das angegebene Konto der Vertragsinhaber ist.

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

    Initiiert eine vertragsübergreifende Interaktion mit target. Anstatt Methoden direkt für das Ziel aufzurufen (z.B. target.foo()), müssen Sie self.call() verwenden, um den Aufruf explizit über die Ledger-Laufzeit weiterzuleiten. Dieser Mechanismus gibt ein sicheres Proxy-Objekt zurück, mit dem entsprechende öffentliche Methoden für den Zielvertrag aufgerufen werden. So wird sichergestellt, dass die Ausführungsgrenzen strikt eingehalten werden und alle payment sicher übermittelt werden. Weitere Informationen finden Sie unter ContractInterface.

    Beim Aufrufen einer mit @gcul.payable versehenen Methode muss ein payment angegeben werden. Bei nicht zahlungspflichtigen Methoden muss er weggelassen werden.

    target muss ein anderer Vertrag sein. Wenn Sie den aktuellen Vertrag über self.call() aufrufen, wird ein Laufzeitfehler ausgelöst. Wenn Sie eine andere Methode für denselben Vertrag aufrufen möchten, rufen Sie sie direkt auf, z. B. self.foo().

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

    Überträgt einen On-Ledger-Betrag direkt vom Guthaben des Vertrags an beneficiary.

  • Contract.id: str

    Eindeutige String-ID des Vertrags.

Klasse gcul.Account

Ein integrierter Typ, der ein Nutzer- oder Vertragskonto im Ledger darstellt. Sie können gcul.Account-Objekte nicht direkt erstellen. Die Laufzeitumgebung erstellt sie für Sie und stellt sie als Funktions- oder Methodenargumente bereit. Wenn Sie ein Konto als Transaktionsargument übergeben möchten, geben Sie einen AccountValue mit der Konto-ID an. Die Laufzeit validiert die ID und wandelt sie in ein vollständiges Kontoobjekt um. Wenn die ID nicht zu einem registrierten Konto gehört, schlägt die Transaktion fehl. So wird sichergestellt, dass Sie nur mit gültigen Konten arbeiten.

Die Definition der Klasse entspricht in etwa Folgendem:

@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

    Eindeutige String-ID des Kontos.

Klasse gcul.Amount

Stellt einen Geldwert dar, der in einer bestimmten Hauptbuchwährung angegeben ist. Instanziieren Sie einen Betrag mit gcul.Amount(value: int, currency: gcul.Currency). Die Definition der Klasse entspricht in etwa Folgendem:

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

  value: int
  currency: Currency

Klasse gcul.Currency

Stellt eine vollständig qualifizierte Währung im Ledger dar. Die Definition der Klasse entspricht in etwa Folgendem:

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

  code: str
  operator_id: str

Klasse gcul.ContractInterface

Die Basisklasse zum Deklarieren der öffentlichen Schnittstelle eines Vertrags. Methoden und Attribute aus der Basisklasse gcul.ContractInterface können in Unterklassen nicht überschrieben werden. Außerdem dürfen die Methoden in der Schnittstellendeklaration keinen Code enthalten, nur ... ist zulässig.

Damit ein Vertrag Methoden für einen anderen im Ledger bereitgestellten Vertrag aufrufen kann, deklarieren Sie eine Schnittstellenklasse, die von gcul.ContractInterface erbt. Methoden-Stubs müssen mit der entsprechenden Deklaration im Zielvertrag übereinstimmen. Dazu gehören Argumente, Rückgabetyp und Decorator-Annotationen wie @gcul.readonly oder @gcul.payable.

Verwenden Sie in Ihrem Vertragscode self.call(target, payment), um eine Methode aus einem anderen Vertrag aufzurufen.

Das folgende Beispiel zeigt, wie ein TokenVendor-Vertrag mit einem anderen Vertrag interagiert, der die ERC20Interface implementiert. Dabei wird self.call() verwendet, um Tokens im Austausch für eine On-Ledger-Währungszahlung an einen Käufer zu übertragen:

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)

Schnittstellentypen können nicht nur als Methodenparameter, sondern auch als Vertragsfelder verwendet werden. Wenn Sie eine Schnittstelle in einem Feld speichern, kann ein Vertrag über Transaktionen hinweg einen Verweis auf einen anderen Vertrag enthalten und diesen Verweis später ersetzen. Im folgenden Beispielvertrag wird der zuvor deklarierte ERC20Interface verwendet:

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

Klasse gcul.AccountField

Ein integrierter generischer Typ zum Deklarieren von Kontofeldern. gcul.AccountField[T] enthält einen separaten Wert vom Typ T für jedes Konto, das dem Vertrag die Speicherberechtigung erteilt hat. Sie verhält sich wie eine Zuordnung mit gcul.Account-Schlüsseln, mit denen Sie einen Wert für jedes spezifische Konto lesen oder schreiben können. Beispiel:

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 muss ein Typ mit einem Standardwert sein. Daher können gcul.Account, gcul.Amount, gcul.Currency und Schnittstellentypen nicht als Werte für Kontofelder verwendet werden. Wenn ein Konto einem Vertrag die Speicherberechtigung gewährt, wird jedes Kontofeld mit dem Standardwert seines Typs für dieses Konto gefüllt.

Einschränkung: Die Indexierung mit einem Konto ist der einzige Vorgang, der von einem Kontofeld unterstützt wird. Das Feld selbst kann keiner Variablen zugewiesen, als Argument übergeben, von einer Funktion zurückgegeben oder iteriert werden.

Einschränkung: Einträge in Kontofeldern können nicht gelöscht werden. Ein gespeicherter Wert kann überschrieben werden, der Eintrag selbst ist jedoch dauerhaft.

Einschränkung: Kontofelder für gcul.sender, gcul.caller und alle gcul.Account, die als Methodenargument übergeben werden (auch innerhalb von Listen oder anderen Containern), werden vor der Methodenausführung geladen. Bei anderen Konten können in einer einzelnen Transaktion Kontofelder für maximal 15 verschiedene (contract, account)-Paare per Lazy Loading geladen werden. Der mehrfache Zugriff auf ein Kontofeld für dasselbe Paar in einer Transaktion wird nur einmal gezählt. Eine Transaktion, die dieses Limit überschreitet, schlägt fehl.

@gcul.readonly

Kennzeichnet eine Vertragsmethode als schreibgeschützt. Methodendeklarationen mit diesem Decorator können den Vertragsstatus nicht ändern oder Währungsübertragungen im Ledger vornehmen. Außerdem können sie in ihren Signaturen keine änderbaren Containertypen wie list oder dict verwenden. Stattdessen müssen sie schreibgeschützte Typdeklarationen wie Sequence oder Mapping verwenden.

@gcul.payable

Kennzeichnet eine Vertragsmethode als zahlungspflichtig. Wenn eine Methode eine On-Ledger-Währungszahlung mit gcul.payment erwartet, muss sie mit diesem Decorator gekennzeichnet werden. Andernfalls schlägt die Transaktion fehl.

gcul.sender: gcul.Account

Enthält einen Verweis auf das Konto, das die aktuelle InvokeContractMethod-Transaktion signiert und gesendet hat. Der Wert von gcul.sender ändert sich während der Transaktionsausführung nicht, auch wenn vertragsübergreifende Aufrufe erfolgen, im Gegensatz zu gcul.caller, das sich auf den unmittelbaren Aufrufer bezieht.

gcul.caller: gcul.Account

Enthält einen Verweis auf den unmittelbaren Aufrufer der aktuellen Methode. Bei direkten Nutzertransaktionen entspricht gcul.caller dem Wert gcul.sender, der auf die Person verweist, die die Transaktion gesendet hat. Bei vertragsübergreifenden Aufrufen entspricht gcul.caller der Konto-ID des aufrufenden Vertrags. Interne Aufrufe innerhalb eines Vertrags ändern den Wert von gcul.caller nicht.

gcul.payment: gcul.Amount

Enthält die Zahlung, die an eine mit @gcul.payable versehene Methode übertragen wurde. Der Zugriff auf gcul.payment innerhalb einer nicht zahlbaren Methode führt zu einem Fehler bei der Kompilierung.

Operatoren

Die meisten in Python verfügbaren Operatoren werden in GCULpy unterstützt und funktionieren wie erwartet.

  • Addition (+) und Subtraktion (-), einschließlich unärer und binärer Formen.
  • Multiplikation (*), Floor-Division (//) und Modulo (%).
  • Potenzierung (**) für positive Exponenten.
  • Vergleiche (<, <=, >, >=, ==, !=).
  • Bitweises AND (&), OR (|), XOR (^), Linksverschiebung (<<), Rechtsverschiebung (>>), Negation (~).
  • Boolesche Operatoren (and, or, not).
  • Roadmap: Objektidentität (is).
  • Einschränkung: Negative Exponenten sind nicht zulässig und führen zu einem Laufzeitfehler.
  • Einschränkung: Die echte Division (/) ist nicht zulässig und führt zu einem Kompilierungsfehler, da sie den Rückgabetyp float hat.

Kontrollfluss

Die meisten Kontrollflussanweisungen aus Python funktionieren in GCULpy mit derselben Semantik:

  • pass ändern.
  • interne Funktionsaufrufe (gleicher Vertrag, nicht rekursiv).
  • assert ändern.
  • if ... then .. else ... ändern.
  • for VAR in CONTAINER ändern.
  • Externe Funktionsaufrufe (an einen anderen Vertrag, nicht rekursiv)
  • break- und continue-Anweisungen
  • raise ändern.
  • Roadmap-try ... except-Ankündigungen.
  • Roadmap-match-Ankündigungen.
  • Roadmap generators- und yield-Anweisungen.
  • Roadmap: Kontextmanager und with-Anweisungen.

Einschränkung: GCULpy ist absichtlich nicht Turing-vollständig, um Endlosschleifen zu verhindern und die statische Analyse zu erleichtern. So wird das durchgesetzt:

  • Keine Endlosschleifen: Iterationen sind nur mit for-Schleifen über endliche Container oder begrenzte Sequenzen mit range(...) zulässig. while-Schleifen sind nicht zulässig. Einige Containeraktualisierungen, z. B. das Hinzufügen oder Entfernen von Elementen zu einer Liste oder von Schlüsseln zu einem Wörterbuch, sind während der Iteration nicht zulässig.
  • Keine Rekursion: Eine Funktion kann sich weder direkt noch indirekt selbst aufrufen. Die Laufzeitumgebung führt sowohl statische als auch Laufzeitprüfungen durch, um die Verwendung von Rekursion zu erkennen und abzulehnen.
  • Kein asynchroner Kontrollfluss: Die Verwendung von async-Primitiven ist nicht zulässig, um die grundlegenden Designprinzipien der Vorhersehbarkeit, Sicherheit und deterministischen Ausführung beizubehalten. Asynchrone Vorgänge erschweren die Nachvollziehbarkeit des Kontrollflusses eines Programms, was häufig zu Sicherheitslücken und Race-Bedingungen führt.

Integrierte Funktionen

GCULpy unterstützt die folgenden integrierten Funktionen:

  • abs(x)

    Gibt den absoluten Wert einer Ganzzahl x zurück. Argumente müssen vom Typ int sein.

  • len(s)

    Gibt die Länge, d. h. die Anzahl der Elemente, eines Strings oder endlichen Containers zurück.

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

    Gibt das größte Element in einem iterierbaren Objekt oder das größte von zwei oder mehr Argumenten zurück. Wenn max() für ein leeres iterierbares Objekt aufgerufen wird, wird ein Laufzeitfehler ausgelöst.

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

    Gibt das kleinste Element in einem iterierbaren Objekt oder das kleinste von zwei oder mehr Argumenten zurück. Wenn min() für ein leeres iterierbares Objekt aufgerufen wird, wird ein Laufzeitfehler ausgelöst.

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

    Generiert eine arithmetische Folge von Ganzzahlen mit einer Ganzzahl step ungleich null.

    Einschränkung: Kann nur inline mit for-Schleifen verwendet werden, z. B. for i in range(10): .... Bereichsobjekte können nicht Variablen zugewiesen oder in Feldern gespeichert, als Argumente an Funktionen übergeben oder mit len() abgefragt werden.

Roadmap Die folgenden integrierten Funktionen stehen auf der Implementierungs-Roadmap:

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()

Versionshinweise

  • 03.11.2026 Die GA-Version des gculpyc-Compilers (v0.2.0) wurde veröffentlicht, einschließlich Unterstützung für vertragsübergreifende Aufrufe, On-Ledger-Währungsübertragungen und -Zahlungen, @gcul.readonly-Methoden, list, zusätzliche Kontrollflussanweisungen und integrierte Funktionen.

  • 28. Januar 2026 Frühe Version des gculpyc-Compilers, die Teilnehmern der privaten Vorschau von Universal Ledger zur Verfügung gestellt wird. Eine Anleitung zur Verwendung des Compilers finden Sie unter Programmierbaren Vertrag bereitstellen.