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
B
C
D |
E
F
H
I
L |
O
P
R |
日
T
Z |
版本資訊
2026 年 11 月 3 日。發布
gculpyc編譯器 (v0.2.0) 的正式發布版,包括支援跨合約呼叫、帳本內貨幣轉帳和付款、@gcul.readonly方法、list、其他控制流程陳述式和內建函式。2026 年 1 月 28 日。
gculpyc編譯器的早期版本,提供給 Universal Ledger 私人預先發布版的參與者。如需使用編譯器的教學課程,請參閱「部署可程式化合約」。