GCULpy 語言

GCULpy 是 Python 的嚴格靜態類型子集,旨在確保安全性、可讀性和可稽核性。其設計刻意限制 Python 的某些動態功能,以防範常見的智慧型合約安全漏洞,並確保合約行為一律可預測。

本頁提供 GCULpy 語言規格的參考資料,涵蓋 Universal Ledger 網路上的合約核心概念和生命週期。

核心概念

下列合約在 GCULpy 中定義了範例 ERC20 代幣:

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] 是帳戶欄位。合約必須明確獲得儲存空間權限,才能寫入為該帳戶保留的儲存空間。資料儲存後,只有合約執行個體可以修改,帳戶無法修改。

限制:欄位有下列儲存空間限制。如果交易超過任一限制,就會失敗。

  • 合約的所有合約欄位共用一個儲存空間項目,總共可容納約 5 MiB 的序列化資料。
  • 合約為每個帳戶儲存的所有帳戶欄位,最多可存放 1 MiB 的序列化資料。

方法

方法定義了合約的可執行邏輯。其行為類似 Python 方法,可讀取或修改合約欄位。

  • __init__:首次部署合約時,系統只會呼叫建構函式一次。每個合約欄位都必須在建構函式中指派值。建構函式不會初始化帳戶欄位;每個帳戶的值會在該帳戶授予儲存空間權限時建立。

  • 私人方法:以底線開頭的方法 (例如 _internal_logic) 為私人方法,只能由同一合約中的其他方法呼叫。Universal Ledger 解譯器會強制執行這項限制。

  • 公開方法:任何不以底線 (_) 開頭的方法都是公開方法。任何使用者只要提交 InvokeContractMethod 交易,即可透過 ROLE_CONTRACT_PARTICIPANT 呼叫公開方法。

限制:公開方法和所有 @gcul.readonly 方法都無法在簽章中使用可變動類型,例如 list 或 dict。請改用唯讀類型宣告,例如 Sequence 或 Mapping。

限制:執行期間,方法在整個網路的運算用量上限會受到限制。超過這項限制會導致交易失敗。

合約生命週期

以下各節將逐步說明 GCULpy 合約生命週期中的一般作業。

部署合約

首先,使用 gculpyc 編譯器編譯 GCULpy 原始碼。接著,具備 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" }
    }
  }
}

網路處理這筆交易時:

  • 建構函式 (即 __init__ 方法) 會使用提供的引數執行,建立新的合約執行個體。
  • 交易的傳送者會成為合約的擁有者。
  • 合約執行個體會永久儲存在總帳中,並指派專屬的合約 ID,做為交易輸出內容的一部分傳回。

授予權限

合約必須先取得儲存權限,才能代表帳戶 (使用者帳戶或其他合約) 將資料儲存在帳戶欄位中。這是重要的安全步驟。

授予使用者帳戶權限

擁有 ROLE_CONTRACT_PARTICIPANT 的使用者可以針對特定合約 ID 提交 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 中指定的合約,而非交易傳送者。
  • 系統會為委派合約 delegate_contract_id 初始化目標合約 contract_id 上定義的帳戶欄位,讓目標合約上的方法在跨合約叫用期間,能安全存取以 gcul.caller 或 gcul.sender 為索引鍵的帳戶欄位。

叫用合約方法

部署合約並授予必要權限後,使用者就能呼叫合約的公開方法,與合約互動。擁有 ROLE_CONTRACT_PARTICIPANT 的使用者可以提交 InvokeContractMethod 交易,指定合約 ID、方法名稱和方法引數值。

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 物件是由執行階段建構及驗證。方法的邏輯可以安全地假設提供的 ID 有效,且參照帳本上的現有帳戶。
  • 如果方法因任何原因失敗,交易就會失敗,且合約狀態「不會更新」。
  • 如果方法成功,合約的更新狀態會記錄在世界狀態中。

語言規格

GCULpy 旨在確保安全性和可預測性,因此不允許使用多項 Python 功能,這些功能會標示「Restriction」標籤。這些限制旨在成為永久語言功能,可讓合約邏輯更容易閱讀、稽核和靜態分析,限制令人驚訝或不安全的行為。

其他功能則會標示「Roadmap」標籤,這些功能已列入實作藍圖,但 gculpyc 編譯器尚未支援。

類型

GCULpy 支援多種常見變數類型,並特別強調靜態類型。

核心價值類型:

  • int、bool、str、None 已支援。
  • 藍圖Decimal、bytes、Enum 都在藍圖中。
  • 限制:不得使用 float 和 complex。
  • 限制 int 值為 64 位元帶正負號整數,範圍從 -263 到 263 - 1。如果整數常值超出這個範圍,會導致編譯時間錯誤,而結果超出這個範圍的運算則會導致執行階段錯誤。
  • 限制 str 值的長度上限為 1,000,000 個字元。如果字串常值過長,會導致編譯時間錯誤。如果將較長的字串做為方法引數傳遞,或做為作業結果產生,會導致執行階段錯誤。

容器類型:

  • 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 啟動跨合約互動。您必須使用 self.call(),明確透過 Ledger 執行階段轉送叫用,而不是直接在目標上呼叫方法 (例如 target.foo())。這項機制會傳回安全的 Proxy 物件,用於叫用目標合約上對應的公開方法,確保嚴格執行執行邊界,並安全傳送任何 payment。詳情請參閱 ContractInterface。

    呼叫以 @gcul.payable 裝飾的方法時,必須提供 payment;非付費方法則必須省略。

    target 必須是不同的合約。透過 self.call() 呼叫目前的合約會引發執行階段錯誤。如要對同一合約呼叫其他方法,請直接呼叫,例如 self.foo()。

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

    將帳本上的金額直接從合約餘額轉移至 beneficiary。

  • Contract.id: str

    合約的專屬字串 ID。

類別 gcul.Account

內建類型,代表總帳上的使用者或合約帳戶。您無法直接建立 gcul.Account 物件,執行階段環境會為您建立這些物件,並以函式或方法引數的形式提供。如要將帳戶做為交易引數傳遞,請提供包含帳戶 ID 的 AccountValue。執行階段會驗證 ID,並將其轉換為完整的帳戶物件。如果 ID 不屬於已註冊的帳戶,交易就會失敗。確保您只會使用有效的帳戶。

類別定義大致相當於:

@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

    帳戶的專屬字串 ID。

類別 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 類別的方法和屬性無法在子類別中覆寫。此外,介面宣告中的方法不得包含任何程式碼,僅允許 ...。

如要讓合約呼叫部署在總帳上的其他合約方法,請宣告繼承自 gcul.ContractInterface 的介面類別。方法虛設常式「必須」與目標合約中的對應宣告相符。包括引數、傳回類型和裝飾器註解,例如 @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

將合約方法註解為唯讀。使用這個裝飾器的宣告方法無法變更合約狀態,或進行帳本內貨幣轉帳。此外,他們也無法在簽章中使用可變動的容器類型,例如 list 或 dict;他們必須改用唯讀類型宣告,例如 Sequence 或 Mapping。

@gcul.payable

為合約方法加上註解,表示必須付款才能執行。如果方法預期使用 gcul.payment 接收帳本內貨幣付款,則必須加上這個裝飾器,否則交易會失敗。

gcul.sender: gcul.Account

保留簽署並提交目前 InvokeContractMethod 交易的帳戶參照。gcul.sender 的值在整個交易執行過程中不會改變,即使涉及跨合約呼叫也是如此,這與參照直接呼叫者的 gcul.caller 不同。

gcul.caller: gcul.Account

保留目前方法直接呼叫端的參照。如果是直接使用者交易,gcul.caller 會比對 gcul.sender,指向提交交易的使用者。如果是跨合約呼叫,gcul.caller 會對應至呼叫合約的帳戶 ID。合約內的內部呼叫不會變更 gcul.caller 的值。

gcul.payment: gcul.Amount

保留轉移至以 @gcul.payable 裝飾的方法的付款。在非應付方法中存取 gcul.payment 會引發編譯時間錯誤。

運算子

GCULpy 支援 Python 中可用的大多數運算子,且運作方式符合預期。

  • 加法 (+) 和減法 (-),包括一元和二元形式。
  • 乘法 (*)、取底除法 (//) 和模數 (%)。
  • 正指數的冪運算 (**)。
  • 比較 (<、<=、>、>=、==、!=)。
  • 位元和 (&)、或 (|)、互斥或 (^)、左移 (<<)、右移 (>>)、否定 (~)。
  • 布林運算 (and、or、not)。
  • 藍圖物件身分 (is)。
  • 限制:不允許使用負指數,否則會引發執行階段錯誤。
  • 限制:不允許使用真除法 (/),因為其傳回類型為 float,會導致編譯時間錯誤。

控制流程

Python 的大多數控制流程陳述式都可在 GCULpy 中使用,且語意相同:

  • pass 陳述式來變更這些使用者的權限。
  • 內部函式呼叫 (相同合約,非遞迴)。
  • assert 陳述式來變更這些使用者的權限。
  • if ... then .. else ... 陳述式來變更這些使用者的權限。
  • for VAR in CONTAINER 陳述式來變更這些使用者的權限。
  • 外部函式呼叫 (至任何其他合約,非遞迴)。
  • break 和 continue 陳述式。
  • raise 陳述式來變更這些使用者的權限。
  • 藍圖try ... except陳述式。
  • 藍圖match陳述式。
  • 藍圖generators和yield陳述式。
  • 藍圖:情境管理工具和 with 陳述式。

限制:GCULpy 刻意不具備圖靈完備性,以防止無限迴圈並利於靜態分析。具體做法如下:

  • 不允許無限迴圈:只能使用 for 迴圈對有限容器或有界序列進行疊代,並搭配 range(...);不允許使用 while 迴圈。在疊代容器時,不允許進行某些容器更新,例如在清單中新增或移除元素,或在字典中新增或移除鍵。
  • 不遞迴:函式無法直接或間接呼叫自身。執行階段環境會執行靜態和執行階段檢查,偵測並拒絕使用遞迴。
  • 無非同步控制流程:為維持可預測性、安全性和確定性執行的核心設計原則,不得使用 async 原始物件。非同步作業會導致程式的控制流程難以推論,通常會造成安全漏洞和競爭狀況。

內建函式

GCULpy 支援下列內建函式:

  • abs(x)

    傳回整數 x 的絕對值。引數必須是 int 類型。

  • len(s)

    傳回字串或有限容器的長度,也就是項目數量。

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

    傳回可疊代項目中的最大項目,或兩個以上引數中的最大值。在空白可疊代項目上呼叫 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()

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

T
tuple()

Z
zip()

版本資訊

  • 2026 年 11 月 3 日。發布 gculpyc 編譯器 (v0.2.0) 的正式發布版,包括支援跨合約呼叫、帳本內貨幣轉帳和付款、@gcul.readonly 方法、list、其他控制流程陳述式和內建函式。

  • 2026 年 1 月 28 日。gculpyc 編譯器的早期版本,提供給 Universal Ledger 私人預先發布版的參與者。如需使用編譯器的教學課程,請參閱「部署可程式化合約」。