Il linguaggio GCULpy

GCULpy è un sottoinsieme di Python con tipi statici rigorosi, progettato per essere sicuro, leggibile e verificabile. Il suo design limita intenzionalmente alcune funzionalità dinamiche di Python per prevenire le vulnerabilità comuni degli smart contract e garantire che il comportamento di un contratto sia sempre prevedibile.

Questa pagina fornisce un riferimento per la specifica del linguaggio GCULpy, che copre i concetti di base e il ciclo di vita di un contratto su una rete Universal Ledger.

Concetti principali

Il seguente contratto definisce un token ERC20 di esempio in 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 contratto GCULpy è una classe che eredita da gcul.Contract. Contiene campi (che memorizzano lo stato) e metodi (logica di elaborazione che opera sui campi).

Campi

Lo stato di un contratto GCULpy viene memorizzato nei campi. Tutti i campi devono essere dichiarati con un tipo statico a livello di classe. Esistono due tipi di campi:

  • I campi del contratto contengono un singolo valore memorizzato con il contratto stesso. Nell'esempio ERC20Token, symbol: str e total_supply: int sono campi del contratto.

  • I campi dell'account memorizzano un valore separato per ogni account utente che interagisce con un contratto. Vengono sempre dichiarati come dizionario (dict) con gcul.Account come chiave, ad esempio balance: dict[gcul.Account, int]. Prima che un contratto possa scrivere nell'account di un utente, l'utente deve concedere esplicitamente l'autorizzazione di archiviazione al contratto. Una volta memorizzati i dati, solo l'istanza del contratto può modificarli o eliminarli, l'utente no.

Metodi

I metodi definiscono la logica eseguibile di un contratto. Si comportano come i metodi Python e possono leggere o modificare i campi del contratto.

  • __init__: il costruttore viene chiamato una sola volta quando il contratto viene eseguito il deployment per la prima volta. Viene utilizzato per impostare lo stato iniziale dei campi del contratto. I campi a cui non viene assegnato un valore nel costruttore ricevono un valore predefinito appropriato, ad esempio 0 per un campo int o un dizionario vuoto per un campo dict.

  • Metodi privati: i metodi che iniziano con un trattino basso (ad esempio, _internal_logic) sono privati e possono essere chiamati solo da altri metodi all'interno dello stesso contratto. L'interprete Universal Ledger applica questo vincolo.

  • Metodi pubblici: qualsiasi metodo che non inizia con un trattino basso (_) è pubblico. I metodi pubblici possono essere chiamati da qualsiasi utente con il ROLE_CONTRACT_PARTICIPANT inviando una InvokeContractMethod transazione.

Ciclo di vita del contratto

Le sezioni che seguono illustrano le operazioni tipiche coinvolte nel ciclo di vita di un contratto GCULpy.

Eseguire il deployment di un contratto

Innanzitutto, compila il codice sorgente GCULpy utilizzando il compilatore gculpyc. Poi, un utente con il ROLE_CONTRACT_CREATOR può inviare una CreateContract transazione per eseguire il deployment del bytecode compilato in una rete Universal Ledger. Per istruzioni dettagliate, consulta il tutorial Eseguire il deployment di un contratto programmabile.

Una transazione di questo tipo avrebbe il seguente aspetto:

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" }
      }
    }
  }
}

Quando la rete elabora questa transazione:

  • Viene eseguito il costruttore, ovvero il metodo __init__, per creare una nuova istanza del contratto.
  • Il mittente della transazione diventa il proprietario del contratto.
  • L'istanza del contratto viene memorizzata in modo permanente nel ledger e le viene assegnato un ID contratto univoco, che viene restituito come parte dell'output della transazione.

Concedere le autorizzazioni

Prima che un contratto possa memorizzare i dati in un campo dell'account per conto di un utente, l'utente deve prima concedere l'autorizzazione di archiviazione. Si tratta di un passaggio di sicurezza fondamentale. Un utente con il ruolo ROLE_CONTRACT_PARTICIPANT può inviare una GrantContractPermissions per un ID contratto specifico.

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

Quando la rete elabora questa transazione:

  • Se il contratto non definisce alcun campo dell'account, la transazione viene rifiutata.
  • Se il contratto definisce i campi dell'account, tutti vengono compilati con i valori predefiniti (ad esempio, contract.balance[gcul.sender] = 0). Questi valori vengono quindi memorizzati nello stato del mondo come parte dei dati dell'account e il mittente della transazione viene registrato come partecipante a questa istanza di contratto specifica.

Chiamare i metodi del contratto

Una volta eseguito il deployment di un contratto e concesse le autorizzazioni necessarie, gli utenti possono interagire con esso chiamando i suoi metodi pubblici. Un utente con un ROLE_CONTRACT_PARTICIPANT può inviare una InvokeContractMethod specificando l'ID contratto, il nome del metodo e i valori degli argomenti.

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 }
      }
    }
  }
}

Quando la rete elabora questa transazione:

  • Viene recuperata l'istanza del contratto associata all'CONTRACT_ID fornito.
  • Viene eseguito il metodo mint(beneficiary=Account("BENEFICIARY_ID"), value=10). L'oggetto Account per il beneficiario viene creato e convalidato dal runtime. La logica del metodo può presupporre in sicurezza che l'ID fornito sia valido e si riferisca a un account esistente nel ledger.
  • Se il metodo non riesce per qualsiasi motivo, la transazione non andrà a buon fine e non verranno apportati aggiornamenti allo stato del contratto.
  • Se il metodo ha esito positivo, lo stato aggiornato del contratto viene registrato nello stato del mondo.

Specifica del linguaggio

GCULpy è progettato per la sicurezza e la prevedibilità e, di conseguenza, non consente diverse funzionalità di Python; queste verranno indicate con l'etichetta Restriction. Queste limitazioni sono intese come funzionalità del linguaggio permanenti, introdotte per rendere la logica del contratto più facile da leggere, controllare e analizzare staticamente, limitando comportamenti sorprendenti o non sicuri.

Altre funzionalità sono indicate con l'etichetta Roadmap, queste sono nella roadmap di implementazione ma non sono ancora supportate dal compilatore gculpyc.

Tipi

GCULpy supporta una serie di tipi di variabili comuni con una forte enfasi sulla digitazione statica.

Tipi di valori principali:

  • int, bool, str, None sono già supportati.
  • Roadmap Decimal, bytes, Enum sono nella roadmap.
  • Restrizione float e complex non sono consentiti.

Tipi di container:

  • dict è già supportato.
  • Roadmap list, tuple, set, dataclass sono nella roadmap.
  • Restrizione I tipi concreti devono essere specificati per i valori in un container, ad esempio dict[str, int] è consentito, ma un semplice dict o dict[str, Any] non lo sono.
  • È supportata la nidificazione dei container, ad esempio dict[str, list[int]].

Restriction Tutte le variabili, inclusi i campi del contratto e dell'account, i parametri delle funzioni e i tipi di restituzione, devono essere definiti e digitati staticamente. Il loro tipo non può essere modificato in fase di runtime e sono supportati solo i tipi concreti. I tipi non possono essere utilizzati come valori, ad esempio non possono essere memorizzati nelle variabili o passati alle funzioni come argomenti. Se provi ad assegnare un valore a un campo non dichiarato, si verifica un errore in fase di compilazione.

Classi ed ereditarietà

Inizialmente, puoi definire solo le classi che sono sottoclassi dirette della classe base gcul.Contract. Questa regola rigorosa impedisce le complessità dell'ereditarietà completa di Python, che può introdurre bug difficili da trovare e rendere il codice difficile da comprendere. Per motivi di sicurezza, se provi a sostituire una proprietà o un metodo da una classe padre, viene generato un errore, fornendo una protezione chiara contro comportamenti imprevisti.

Roadmap GCULpy offrirà maggiore flessibilità mentre mantenendo i suoi principi fondamentali. La roadmap include il supporto per l'ereditarietà singola nelle classi definite dall'utente con la sostituzione dei metodi gestita esplicitamente con un decoratore @override. Inoltre, l'elemento integrato super() sarà supportato solo nella sua forma senza argomenti, per garantire operazioni dirette e prevedibili.

Il modulo gcul

GCULpy fornisce un modulo gcul integrato con tipi e variabili essenziali per lo sviluppo di contratti.

Classe gcul.Contract

La classe base per tutti i contratti. Non puoi creare istanze direttamente; i contratti vengono istanziati solo tramite le CreateContract. I metodi e le proprietà della classe base gcul.Contract non possono essere sostituiti nelle sottoclassi.

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

    Restituisce True se l'account fornito è il proprietario del contratto.

Classe gcul.Account

Un tipo integrato che rappresenta un account utente nel ledger. Non puoi creare oggetti gcul.Account direttamente; l'ambiente di runtime li crea per te e li fornisce come argomenti di funzioni o metodi. Quando passi un ID account come argomento di transazione, il runtime lo convalida automaticamente. Se si tratta di un ID valido per un account registrato, viene convertito in un oggetto account completo. In caso contrario, la transazione non andrà a buon fine. In questo modo, puoi lavorare solo con account validi.

La definizione della classe è approssimativamente equivalente a:

@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

Una variabile speciale, disponibile in qualsiasi metodo, che contiene un riferimento all'account che ha firmato e inviato la transazione corrente.

Roadmap Migliorare la capacità degli sviluppatori di gestire e interagire con contratti e account: potrai passare i riferimenti agli oggetti contratto come argomenti, memorizzarli nei campi, accedere al loro ID univoco (contract.id: str). Allo stesso modo, sarà possibile memorizzare i riferimenti agli oggetti account e recuperare i relativi ID.

Operatori

La maggior parte degli operatori disponibili in Python è supportata in GCULpy e funziona come previsto.

  • Addizione (+) e sottrazione (-), incluse le forme unarie e binarie.
  • Moltiplicazione (*), divisione intera (//) e modulo (%).
  • Esponenziazione (**) per esponenti positivi.
  • Confronti (<, <=, >, >=, ==, !=).
  • Bitwise and (&), or (|), xor (^), left shift (<<), right shift (>>), negation (~).
  • Operazioni booleane (and, or, not).
  • Roadmap Identità dell'oggetto (is).
  • Restriction Gli esponenti negativi non sono consentiti e generano un errore di runtime error.
  • Restrizione La divisione reale (/), poiché ha un tipo restituito float, non è consentita e genera un errore in fase di compilazione.

Flusso di controllo

La maggior parte delle istruzioni di flusso di controllo di Python funziona in GCULpy con la stessa semantica:

  • Istruzioni pass.
  • Chiamate di funzioni interne (stesso contratto, non ricorsive).
  • Istruzioni assert.
  • Istruzioni if ... then .. else ....
  • Istruzioni for VAR in CONTAINER.
  • Roadmap Chiamate di funzioni esterne (a qualsiasi altro contratto, non ricorsive).
  • Roadmap break e continue istruzioni.
  • Roadmap raise e try ... except istruzioni.
  • Roadmap match istruzioni.
  • Roadmap generators e yield istruzioni.
  • Roadmap Gestori di contesto e with istruzioni.

Restrizione GCULpy è intenzionalmente Turing-incompleto per impedire loop infiniti, facilitare l'analisi statica e garantire costi di elaborazione delle transazioni prevedibili. Ecco come lo applica:

  • Nessun loop infinito:l'iterazione è consentita solo utilizzando i loop for su container finiti; i loop while non sono consentiti. Alcuni aggiornamenti dei container, ad esempio l'aggiunta o la rimozione di elementi a un elenco o di chiavi a un dizionario, non sono consentiti durante l'iterazione.
  • Nessuna ricorsione:una funzione non può chiamare se stessa, né direttamente né indirettamente. L'ambiente di runtime esegue controlli statici e di runtime per rilevare e rifiutare l'utilizzo della ricorsione.
  • Nessun flusso di controllo asincrono:l'utilizzo di primitive async non è consentito per mantenere i principi di progettazione fondamentali di prevedibilità, sicurezza ed esecuzione deterministica. Le operazioni asincrone rendono difficile ragionare sul flusso di controllo di un programma, spesso portando a vulnerabilità e condizioni di gara.

Funzioni integrate

Roadmap Ecco uno sguardo alla nostra roadmap per le funzioni integrate con le funzionalità più fondamentali e di uso comune per creare con sicurezza.

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()

Note di rilascio