Langage GCULpy

GCULpy est un sous-ensemble de Python à typage statique strict, conçu pour être sûr, lisible et auditable. Sa conception limite intentionnellement certaines fonctionnalités dynamiques de Python pour éviter les vulnérabilités courantes des contrats intelligents et garantir que le comportement d'un contrat est toujours prévisible.

Cette page fournit une référence pour la spécification du langage GCULpy, qui couvre les concepts de base et le cycle de vie d'un contrat sur un réseau Universal Ledger.

Concepts fondamentaux

Le contrat suivant définit un exemple de jeton ERC20 dans GCULpy :

import gcul

class ERC20Token(gcul.Contract):
    """Sample ERC20 implementation for the Universal Ledger."""

    symbol: str
    total_supply: int
    balance: dict[gcul.Account, int]

    def __init__(self, symbol: str):
        self.symbol = symbol

    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

Un contrat GCULpy est une classe qui hérite de gcul.Contract. Il contient des champs (qui stockent l'état) et des méthodes (logique de traitement qui opère sur les champs).

Champs

L'état d'un contrat GCULpy est stocké dans des champs. Tous les champs doivent être déclarés avec un type statique au niveau de la classe. Il existe deux types de champs :

  • Les champs de contrat contiennent une seule valeur stockée avec le contrat lui-même. Dans l'exemple ERC20Token, symbol: str et total_supply: int sont des champs de contrat.

  • Les champs de compte stockent une valeur distincte pour chaque compte utilisateur interagissant avec un contrat. Elles sont toujours déclarées en tant que dictionnaire (dict) avec gcul.Account comme clé, par exemple balance: dict[gcul.Account, int]. Avant qu'un contrat puisse écrire dans le compte d'un utilisateur, celui-ci doit explicitement accorder l'autorisation de stockage au contrat. Une fois les données stockées, seule l'instance de contrat peut les modifier ou les supprimer. L'utilisateur ne le peut pas.

Méthodes

Les méthodes définissent la logique exécutable d'un contrat. Ils se comportent comme des méthodes Python et peuvent lire ou modifier les champs du contrat.

  • __init__ : le constructeur n'est appelé qu'une seule fois lors du premier déploiement du contrat. Il permet de définir l'état initial des champs du contrat. Les champs auxquels aucune valeur n'est attribuée dans le constructeur reçoivent une valeur par défaut appropriée, par exemple 0 pour un champ int ou un dictionnaire vide pour un champ dict.

  • Méthodes privées : les méthodes commençant par un trait de soulignement (par exemple, _internal_logic) sont privées et ne peuvent être appelées que par d'autres méthodes du même contrat. L'interpréteur Universal Ledger applique cette contrainte.

  • Méthodes publiques : toute méthode ne commençant pas par un trait de soulignement (_) est publique. Les méthodes publiques peuvent être appelées par n'importe quel utilisateur disposant de ROLE_CONTRACT_PARTICIPANT en envoyant une transaction InvokeContractMethod.

Cycle de vie des contrats

Les sections suivantes décrivent les opérations typiques impliquées dans le cycle de vie d'un contrat GCULpy.

Déployer un contrat

Commencez par compiler votre code source GCULpy à l'aide du compilateur gculpyc. Un utilisateur disposant du rôle ROLE_CONTRACT_CREATOR peut ensuite envoyer une transaction CreateContract pour déployer le bytecode compilé sur un réseau Universal Ledger. Pour obtenir des instructions détaillées, consultez le tutoriel Déployer un contrat programmable.

Une telle transaction se présenterait comme suit :

client_transaction {
  sender_id: "OWNER_ACCOUNT_ID"
  app {
    [type.googleapis.com/google.cloud.universalledger.v1.CreateContract] {
      contract_bytes: "COMPILED_BYTECODE"
      arguments {
        key: "symbol"
        value: { str_value: "US02079K1079" }
      }
    }
  }
}

Lorsque le réseau traite cette transaction :

  • Le constructeur, c'est-à-dire la méthode __init__, est exécuté pour créer une instance de contrat.
  • L'expéditeur de la transaction devient le propriétaire du contrat.
  • L'instance de contrat est stockée de manière permanente dans le grand livre et reçoit un ID de contrat unique, qui est renvoyé dans la sortie de la transaction.

Accorder des autorisations

Avant qu'un contrat puisse stocker des données dans un champ de compte au nom d'un utilisateur, celui-ci doit d'abord lui accorder l'autorisation de stockage. Il s'agit d'une étape de sécurité essentielle. Un utilisateur disposant d'un ROLE_CONTRACT_PARTICIPANT peut envoyer une transaction GrantContractPermissions pour un ID de contrat spécifique.

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  app {
    [type.googleapis.com/google.cloud.universalledger.v1.GrantContractPermissions] {
      contract_id: "CONTRACT_ID"
      permissions: CONTRACT_PERMISSION_STORAGE
    }
  }
}

Lorsque le réseau traite cette transaction :

  • Si le contrat ne définit aucun champ de compte, la transaction est refusée.
  • Si le contrat définit des champs de compte, ils sont tous renseignés avec des valeurs par défaut (par exemple, contract.balance[gcul.sender] = 0). Ces valeurs sont ensuite stockées dans l'état du monde en tant que données du compte, et l'expéditeur de la transaction est enregistré comme participant à cette instance de contrat spécifique.

Appeler des méthodes de contrat

Une fois qu'un contrat est déployé et que les autorisations nécessaires sont accordées, les utilisateurs peuvent interagir avec lui en appelant ses méthodes publiques. Un utilisateur disposant d'un ROLE_CONTRACT_PARTICIPANT peut envoyer une transaction InvokeContractMethod en spécifiant l'ID du contrat, le nom de la méthode et les valeurs des arguments.

client_transaction {
  sender_id: "PARTICIPANT_ACCOUNT_ID"
  app {
    [type.googleapis.com/google.cloud.universalledger.v1.InvokeContractMethod] {
      contract_id: "CONTRACT_ID"
      method_name: "mint"
      arguments {
        key: "beneficiary"
        value: { account_id: "BENEFICIARY_ID" }
      }
      arguments {
        key: "value"
        value: { int_value: 10 }
      }
    }
  }
}

Lorsque le réseau traite cette transaction :

  • L'instance de contrat associée à l'CONTRACT_ID fourni est récupérée.
  • La méthode mint(beneficiary=Account("BENEFICIARY_ID"), value=10) est exécutée. L'objet Account pour le bénéficiaire est créé et validé par le runtime. La logique de la méthode peut supposer sans risque que l'ID fourni est valide et fait référence à un compte existant dans le grand livre.
  • Si la méthode échoue pour une raison quelconque, la transaction échouera et aucune modification ne sera apportée à l'état du contrat.
  • Si la méthode réussit, l'état mis à jour du contrat est enregistré dans l'état du monde.

Spécification de la langue

GCULpy est conçu pour la sécurité et la prévisibilité. À ce titre, il interdit plusieurs fonctionnalités Python, qui seront signalées par le libellé Restriction. Ces restrictions sont destinées à être des fonctionnalités linguistiques permanentes, introduites pour rendre la logique des contrats plus facile à lire, à auditer et à analyser de manière statique, en limitant les comportements surprenants ou dangereux.

Les autres fonctionnalités sont indiquées par le libellé Roadmap. Elles figurent sur la feuille de route d'implémentation, mais ne sont pas encore compatibles avec le compilateur gculpyc.

Types

GCULpy est compatible avec une gamme de types de variables courants, en mettant fortement l'accent sur le typage statique.

Types de valeurs de base :

  • int, bool, str et None sont déjà compatibles.
  • Feuille de route : Decimal, bytes, Enum sont prévus.
  • Les restrictions float et complex ne sont pas autorisées.

Types de conteneurs :

  • dict est déjà pris en charge.
  • Les actions de routage list, tuple, set et dataclass sont prévues.
  • Restriction : des types concrets doivent être spécifiés pour les valeurs d'un conteneur. Par exemple, dict[str, int] est autorisé, mais dict ou dict[str, Any] ne le sont pas.
  • L'imbrication de conteneurs est acceptée, par exemple dict[str, list[int]].

Restriction : toutes les variables, y compris les champs de contrat et de compte, les paramètres de fonction et les types de retour, doivent être définies et typées de manière statique. Leur type ne peut pas être modifié au moment de l'exécution et seuls les types concrets sont acceptés. Les types ne peuvent pas être utilisés comme valeurs. Par exemple, ils ne peuvent pas être stockés dans des variables ni transmis à des fonctions en tant qu'arguments. Toute tentative d'attribution d'une valeur à un champ non déclaré génère une erreur de compilation.

Classes et héritage

Au début, vous ne pouvez définir que des classes qui sont des sous-classes directes de la classe de base gcul.Contract. Cette règle stricte évite les complexités de l'héritage Python complet, qui peuvent introduire des bugs difficiles à trouver et rendre le code difficile à comprendre. Pour des raisons de sécurité, toute tentative de remplacement d'une propriété ou d'une méthode d'une classe parente génère une erreur, ce qui constitue une protection claire contre les comportements inattendus.

La feuille de route de GCULpy prévoit d'offrir plus de flexibilité tout en conservant ses principes de base. La feuille de route inclut la prise en charge de l'héritage unique sur les classes définies par l'utilisateur avec la substitution de méthode gérée explicitement avec un décorateur @override. De plus, l'élément intégré super() ne sera compatible qu'avec sa forme sans argument, afin de garantir des opérations directes et prévisibles.

Le module gcul

GCULpy fournit un module gcul intégré avec les types et variables essentiels pour le développement de contrats.

classe gcul.Contract

Classe de base pour tous les contrats. Vous ne pouvez pas créer d'instances directement. Les contrats ne sont instanciés que par le biais de transactions CreateContract. Les méthodes et les propriétés de la classe de base gcul.Contract ne peuvent pas être remplacées dans les sous-classes.

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

    Renvoie True si le compte fourni est le propriétaire du contrat.

classe gcul.Account

Type intégré représentant un compte utilisateur dans le grand livre. Vous ne pouvez pas créer d'objets gcul.Account directement. L'environnement d'exécution les crée pour vous et les fournit en tant qu'arguments de fonction ou de méthode. Lorsque vous transmettez un ID de compte en tant qu'argument de transaction, le runtime le valide automatiquement. S'il s'agit d'un identifiant valide pour un compte enregistré, il est converti en objet de compte complet. Sinon, la transaction échoue. Vous ne travaillerez ainsi qu'avec des comptes valides.

La définition de la classe est à peu près équivalente à :

@dataclasses.dataclass(frozen=True)
class Account:
  """A valid account on the ledger."""

  id: str  # The ID of the account as a string.

gcul.sender: gcul.Account

Variable spéciale, disponible dans n'importe quelle méthode, qui contient une référence au compte ayant signé et envoyé la transaction actuelle.

Roadmap : amélioration de la capacité des développeurs à gérer les contrats et les comptes et à interagir avec eux. Vous pourrez transmettre des références à des objets de contrat en tant qu'arguments, les stocker dans des champs et accéder à leur ID unique (contract.id: str). De même, il sera possible de stocker des références à des objets de compte et de récupérer leurs ID.

Opérateurs

La plupart des opérateurs disponibles dans Python sont compatibles avec GCULpy et fonctionnent comme prévu.

  • Addition (+) et soustraction (-), y compris les formes unaires et binaires.
  • Multiplication (*), division entière (//) et modulo (%).
  • Exponentiation (**) pour les exposants positifs.
  • Comparaisons (<, <=, >, >=, ==, !=).
  • Opérateurs bit à bit : AND (&), OR (|), XOR (^), décalage à gauche (<<), décalage à droite (>>), négation (~).
  • Opérations booléennes (and, or, not).
  • Roadmap : identité de l'objet (is).
  • Restriction : les exposants négatifs ne sont pas autorisés et génèrent une erreur d'exécution.
  • La restriction de la division réelle (/), qui a un type renvoyé float, n'est pas autorisée et entraîne une erreur de compilation.

Flux de contrôle

La plupart des instructions de flux de contrôle de Python fonctionnent dans GCULpy avec la même sémantique :

  • pass.
  • appels de fonction internes (même contrat, non récursifs).
  • assert.
  • if ... then .. else ....
  • for VAR in CONTAINER.
  • Appels de fonctions externes Roadmap (à tout autre contrat, non récursif).
  • Instructions Roadmap break et continue
  • Instructions Roadmap raise et try ... except
  • Feuille de route : déclarations match.
  • Instructions Roadmap generators et yield
  • Gestionnaires de contexte Roadmap et instructions with.

Restriction : GCULpy est volontairement Turing-incomplet pour éviter les boucles infinies, faciliter l'analyse statique et garantir des coûts de traitement des transactions prévisibles. Voici comment il l'applique :

  • Pas de boucles infinies : l'itération n'est autorisée qu'à l'aide de boucles for sur des conteneurs finis. Les boucles while ne sont pas autorisées. Certaines mises à jour de conteneurs, par exemple l'ajout ou la suppression d'éléments dans une liste ou de clés dans un dictionnaire, ne sont pas autorisées lors de l'itération.
  • Pas de récursivité : une fonction ne peut pas s'appeler elle-même, que ce soit directement ou indirectement. L'environnement d'exécution effectue des vérifications statiques et d'exécution pour détecter et refuser l'utilisation de la récursivité.
  • Pas de flux de contrôle asynchrone : l'utilisation de primitives async n'est pas autorisée pour respecter les principes de conception fondamentaux de prévisibilité, de sécurité et d'exécution déterministe. Les opérations asynchrones rendent difficile la compréhension du flux de contrôle d'un programme, ce qui conduit souvent à des failles et à des conditions de concurrence.

Fonctions intégrées

Feuille de route : voici notre feuille de route pour les fonctions intégrées, avec les fonctionnalités les plus fondamentales et les plus couramment utilisées pour créer des applications en toute confiance.

A
abs()
all()
any()

B
bin()
bool()
bytes()

C
chr()

D
dict()
divmod()

E
enumerate()

F
format()
frozenset()

H
hash()
hex()

I
id()
int()

L
len()
list()

M
max()
min()

O
oct()
ord()

P
pow()
property()

R
range()
repr()
reversed()

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

T
tuple()

Z
zip()

Notes de version

  • 28 janvier 2026 Première version du compilateur gculpyc mise à la disposition des participants à la version Preview privée Universal Ledger. Pour suivre un tutoriel sur l'utilisation du compilateur, consultez Déployer un contrat programmable.