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 解释器会强制执行此限制。

  • 公共方法:任何不以下划线 (_) 开头的方法都是公共方法。任何具有 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
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()

版本说明

  • 2026 年 11 月 3 日。发布了 gculpyc 编译器 (v0.2.0) 的正式版,包括对跨合约调用、账本内货币转账和付款、@gcul.readonly 方法、list、其他控制流语句和内置函数的支持。

  • 2026 年 1 月 28 日。gculpyc 编译器的早期版本已向 Universal Ledger 非公开预览版的参与者提供。如需查看使用编译器的教程,请参阅部署可编程合约。