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 解释器会强制执行此限制。公共方法:任何不以下划线 (
_) 开头的方法都是公共方法。任何具有ROLE_CONTRACT_PARTICIPANT的用户都可以通过提交 InvokeContractMethod 事务来调用公共方法。
限制 公共方法和所有 @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,该 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中指定的合约,而不是交易发送者。 - 在目标合约
contract_id上定义的账号字段会针对委托合约delegate_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 功能;这些功能将带有限制标签。这些限制旨在作为永久性语言功能,旨在使合约逻辑更易于阅读、审核和静态分析,从而限制令人惊讶或不安全的行为。
其他功能带有路线图标签,这些功能在实现路线图上,但 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 继承带来的复杂性,因为这种复杂性可能会引入难以发现的 bug,并使代码难以推理。出于安全考虑,尝试替换父类中的属性或方法会引发错误,从而提供明确的保护措施,防止出现意外行为。
路线图: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()通过账本运行时明确路由调用。此机制会返回一个安全代理对象,用于在目标合约上调用相应的公共方法,从而确保严格执行执行边界,并安全地传递任何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 |
S
T
Z |
版本说明
2026 年 11 月 3 日。发布了
gculpyc编译器 (v0.2.0) 的正式版,包括对跨合约调用、账本内货币转账和付款、@gcul.readonly方法、list、其他控制流语句和内置函数的支持。2026 年 1 月 28 日。
gculpyc编译器的早期版本已向 Universal Ledger 非公开预览版的参与者提供。如需查看使用编译器的教程,请参阅部署可编程合约。