A GCULpy é um subconjunto estrito de tipagem estática do Python, projetado para ser seguro, legível e auditável. O design dela limita intencionalmente certos recursos dinâmicos do Python para evitar vulnerabilidades comuns de contratos inteligentes e garantir que o comportamento de um contrato seja sempre previsível.
Nesta página, apresentamos uma referência para a especificação da linguagem GCULpy, que abrange os conceitos principais e o ciclo de vida de um contrato em uma rede de Universal Ledger.
Principais conceitos
O contrato a seguir define um exemplo de token ERC20 em GCULpy:
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
Um contrato GCULpy é uma classe que herda de gcul.Contract. Ela
contém campos (estado de armazenamento) e métodos (lógica de processamento que
opera em campos).
Campos
O estado em um contrato GCULpy é armazenado em campos. Todos os campos precisam ser declarados com um tipo estático no nível da classe. Há dois tipos de campos:
Os campos de contrato contêm um único valor armazenado com o próprio contrato. No exemplo
ERC20Token,symbol: stretotal_supply: intsão campos de contrato.Os campos de conta têm um valor separado para cada conta que interage com o contrato. Eles são declarados com
gcul.AccountField[T], em queTé o tipo do valor por conta. No exemploERC20Token,balance: gcul.AccountField[int]é um campo de conta. Antes que um contrato possa gravar no espaço de armazenamento reservado para essa conta, a permissão de armazenamento precisa ser explicitamente concedida ao contrato. Depois que os dados são armazenados, apenas a instância do contrato pode modificá-los, não a conta.
Os campos de restrição têm os seguintes limites de armazenamento. Uma transação que exceda qualquer um dos limites falhará.
- Todos os campos de um contrato compartilham uma única entrada de armazenamento, que pode conter cerca de 5 MiB de dados serializados no total.
- Todos os campos de conta que um contrato armazena para cada conta individual podem conter no máximo 1 MiB de dados serializados no total.
Métodos
Os métodos definem a lógica executável de um contrato. Eles se comportam como métodos do Python e podem ler ou modificar os campos do contrato.
__init__: o construtor é chamado apenas uma vez quando o contrato é implantado pela primeira vez. Cada campo de contrato precisa receber um valor no construtor. O construtor não inicializa os campos da conta. O valor de cada conta é criado quando ela concede a permissão de armazenamento.Métodos particulares: os métodos que começam com um sublinhado (por exemplo,
_internal_logic) são particulares e só podem ser chamados por outros métodos no mesmo contrato. O interpretador do Universal Ledger aplica essa restrição.Métodos públicos: qualquer método que não comece com um sublinhado (
_) é público. Os métodos públicos podem ser chamados por qualquer usuário com oROLE_CONTRACT_PARTICIPANTenviando uma transação InvokeContractMethod.
Restrição: os métodos públicos e todos os métodos
@gcul.readonly não podem usar tipos mutáveis nas assinaturas, como list ou dict. Em vez disso, use declarações de tipo somente leitura, como Sequence ou Mapping.
Restrição: durante a execução, os métodos são limitados a um limite superior de uso de computação em toda a rede. Exceder esse limite resulta em uma falha de transação.
Ciclo de vida do contrato
As seções a seguir mostram as operações típicas envolvidas no ciclo de vida de um contrato do GCULpy.
Implantar um contrato
Primeiro, compile o código-fonte do GCULpy usando o compilador
gculpyc. Em seguida, um usuário com o ROLE_CONTRACT_CREATOR pode enviar uma transação CreateContract para implantar o bytecode compilado em uma rede do Universal Ledger. Para
instruções detalhadas, consulte o
tutorial
Implantar um contrato programável.
Uma transação desse tipo seria assim:
client_transaction {
sender_id: "OWNER_ACCOUNT_ID"
create_contract_transaction {
contract_bytes: "COMPILED_BYTECODE"
init_arguments {
key: "symbol"
value: { string_value: "US02079K1079" }
}
}
}
Quando a rede processa essa transação:
- O construtor, que é o método
__init__, é executado com os argumentos fornecidos para criar uma nova instância de contrato. - O remetente da transação se torna o proprietário do contrato.
- A instância do contrato é armazenada permanentemente no livro-razão e recebe um ID de contrato exclusivo, que é retornado como parte da saída da transação.
Conceder permissões
Antes que um contrato possa armazenar dados em um campo de conta em nome de uma conta, seja uma conta de usuário ou outro contrato, a permissão de armazenamento precisa ser concedida ao contrato que armazena os dados. Essa é uma etapa de segurança crítica.
Como conceder permissões para uma conta de usuário
Um usuário com um ROLE_CONTRACT_PARTICIPANT pode enviar uma transação GrantContractPermissions para um ID de contrato específico:
client_transaction {
sender_id: "PARTICIPANT_ACCOUNT_ID"
grant_contract_permissions_transaction {
contract_id: "CONTRACT_ID"
permissions: CONTRACT_PERMISSION_STORAGE
}
}
Quando a rede processa essa transação:
- Se o contrato não definir nenhum campo de conta, a transação será rejeitada.
- Se o contrato definir campos de conta, todos eles serão preenchidos com
valores padrão (por exemplo,
contract.balance[gcul.sender] = 0). Esses valores serão armazenados no estado global como parte dos dados da conta.
Como conceder permissões para um contrato de delegado
Um usuário com um ROLE_CONTRACT_PARTICIPANT pode enviar uma transação GrantContractPermissions para permitir que um contrato armazene dados em nome de outro contrato que ele possui:
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
}
}
Quando a rede processa essa transação:
- O remetente da transação, ou seja,
sender_id, precisa ser o proprietário do contrato identificado pordelegate_contract_id. - Um contrato não pode conceder permissões de armazenamento a si mesmo. Portanto,
delegate_contract_idprecisa ser diferente decontract_id. - As permissões de armazenamento são concedidas exclusivamente ao contrato especificado em
delegate_contract_id, e não ao remetente da transação. - Os campos de conta definidos no contrato de destino,
contract_id, são inicializados para o contrato delegado,delegate_contract_id, permitindo que os métodos no contrato de destino acessem com segurança os campos de conta indexados porgcul.callerougcul.senderdurante invocações entre contratos.
Invocar métodos de contrato
Depois que um contrato é implantado e as permissões necessárias são concedidas, os usuários podem
interagir com ele chamando os métodos públicos. Um usuário com um
ROLE_CONTRACT_PARTICIPANT pode enviar uma transação
InvokeContractMethod
especificando o ID do contrato, o nome do método e os valores do argumento do método.
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 }
}
}
}
Quando a rede processa essa transação:
- A instância de contrato associada ao
CONTRACT_IDfornecido é recuperada. - O método
mint(beneficiary=Account("BENEFICIARY_ID"), value=10)é executado. O objetoAccountdo beneficiário é criado e validado pelo ambiente de execução. A lógica do método pode presumir com segurança que o ID fornecido é válido e se refere a uma conta existente no livro. - Se o método falhar por qualquer motivo, a transação vai falhar e nenhuma atualização será feita no estado do contrato.
- Se o método for bem-sucedido, o estado atualizado do contrato será registrado no estado global.
Especificação de linguagem
A GCULpy foi projetada para segurança e previsibilidade e, como tal, não permite vários recursos do Python. Eles serão chamados com o rótulo Restrição. Essas restrições são recursos permanentes da linguagem, introduzidos para facilitar a leitura, auditoria e análise estática da lógica do contrato, limitando comportamentos surpreendentes ou não seguros.
Outros recursos são identificados com o rótulo Roadmap. Eles estão no
roteiro de implementação, mas ainda não são compatíveis com o compilador gculpyc.
Tipos
O GCULpy é compatível com uma variedade de tipos de variáveis comuns, com uma forte ênfase na tipagem estática.
Tipos de valores principais:
int,bool,streNonejá são compatíveis.- Roteiro
Decimal,bytes,Enumestão no roteiro. - Restrição:
floatecomplexnão são permitidos. - Restrição: os valores
intsão números inteiros assinados de 64 bits, de -263 a 263 - 1. Um literal de número inteiro fora desse intervalo resultará em um erro de tempo de compilação, e uma operação com resultado fora desse intervalo resultará em um erro de tempo de execução. - Restrição: os valores de
strpodem ter no máximo 1.000.000 de caracteres. Uma string literal mais longa vai resultar em um erro de tempo de compilação. Transmitir uma string mais longa como um argumento de método ou produzir uma como resultado de uma operação resultará em um erro de tempo de execução.
Tipos de contêiner:
listedict:list[T]: uma sequência mutável. Oferece suporte à indexação, por exemplo,my_list[0], e ao métodoappend().dict[K, V]: um mapeamento mutável. Oferece suporte à indexação, por exemplo,my_dict[key], e ao métodopop().
collections.abc.Sequenceecollections.abc.Mapping:collections.abc.Sequence[T]: uma referência somente leitura a uma sequência. Oferece suporte à leitura de elementos por índice, por exemplo,my_seq[0], mas os elementos não podem ser modificados ou excluídos.list[T]é um subtipo deSequence[T]. Portanto, é possível transmitir, retornar ou atribuir um valor ou literal de listalist[T]a um destino digitado comoSequence[T], mas não é possível atribuir umSequence[T]a umlist[T].collections.abc.Mapping[K, V]: uma referência somente leitura a um mapeamento. Oferece suporte à leitura de entradas por chave, por exemplo,my_map[key], mas as entradas não podem ser modificadas ou excluídas.dict[K, V]é um subtipo deMapping[K, V]. Portanto, é possível transmitir, retornar ou atribuir um valordict[K, V]ou um literal de dicionário a um destino digitado comoMapping[K, V], mas não é possível atribuir umMapping[K, V]a umdict[K, V].
- Roteiro
tuple,set,dataclassestão no roteiro. - Restrição: os tipos concretos precisam ser especificados para valores em
um contêiner. Por exemplo,
dict[str, int]é permitido, mas umdictoudict[str, Any]simples não. - Roteiro: fatiamento de listas, por exemplo,
my_list[1:3]. - Roteiro Outros métodos, como
.get(),.keys(),.values(),.items(),.clear()ou.extend().
Restrição: todas as variáveis, incluindo campos de contrato e conta, parâmetros de função e tipos de retorno, precisam ser definidas e digitadas estaticamente. O tipo deles não pode ser alterado no momento da execução, e apenas tipos concretos são aceitos. Os tipos não podem ser usados como valores. Por exemplo, eles não podem ser armazenados em variáveis ou transmitidos para funções como argumentos. Tentar atribuir um valor a um campo não declarado resultará em um erro de tempo de compilação.
Restrição: uma variável local precisa ser atribuída antes de ser lida. As variáveis locais não têm valor padrão. Portanto, ler uma delas antes de ser atribuída resulta em um erro de tempo de execução.
Classes e herança
Só é possível definir classes que sejam subclasses diretas de
gcul.Contract ou gcul.ContractInterface.
Essa regra estrita evita as complexidades da herança completa do Python, que pode introduzir bugs difíceis de encontrar e dificultar a compreensão do código. Por segurança, tentar substituir uma propriedade ou um método de uma
classe pai vai gerar um erro, fornecendo uma proteção clara contra comportamentos
inesperados.
Plano de lançamento: a GCULpy vai oferecer mais flexibilidade, mantendo
os princípios fundamentais. O roteiro inclui suporte para
herança única em classes definidas pelo usuário com substituição de método gerenciada
explicitamente com um decorador @override. Além disso, o super() integrado só será compatível com o formato livre argumentos para garantir operações diretas e previsíveis.
O módulo gcul
A GCULpy oferece um módulo gcul integrado com tipos, decoradores e variáveis essenciais para o desenvolvimento de contratos.
classe gcul.Contract
A classe de base para todos os contratos. Não é possível criar instâncias dele diretamente.
Os contratos só são instanciados por meio de
transações
CreateContract. Os métodos e as propriedades da classe de base gcul.Contract não podem ser substituídos em subclasses.
Contract.is_owner(account: Account) -> boolRetorna
Truese a conta fornecida for a proprietária do contrato.Contract.call(target: ContractInterface, payment: Amount | None = None) -> ContractMethodsInicia uma interação entre contratos com
target. Em vez de chamar métodos diretamente no destino (por exemplo,target.foo()), useself.call()para rotear explicitamente a invocação pelo ambiente de execução do ledger. Esse mecanismo retorna um objeto de proxy seguro usado para invocar métodos públicos correspondentes no contrato de destino, garantindo que os limites de execução sejam aplicados de forma estrita e que qualquerpaymentseja entregue com segurança. Consulte ContractInterface para mais detalhes.Um
paymentprecisa ser fornecido ao chamar um método decorado com@gcul.payablee omitido para métodos não pagáveis.targetprecisa ser um contrato diferente. Chamar o contrato atual por meio deself.call()gera um erro de tempo de execução. Para chamar outro método no mesmo contrato, chame-o diretamente, por exemplo,self.foo().Contract.native_transfer(beneficiary: Account, amount: Amount) -> NoneTransfere um valor no livro diretamente do saldo do contrato para
beneficiary.Contract.id: strID de string exclusivo do contrato.
classe gcul.Account
Um tipo integrado que representa uma conta de usuário ou de contrato no livro-razão. Não é possível criar objetos gcul.Account diretamente. O ambiente de execução os cria para você e os fornece como argumentos de função ou método. Para transmitir uma
conta como um argumento de transação, forneça um
AccountValue
que contenha o ID da conta. O ambiente de execução valida o ID e o converte em um
objeto de conta completo. Se o ID não pertencer a uma conta registrada, a transação vai falhar. Isso garante que você só trabalhe com contas válidas.
A definição da classe é mais ou menos equivalente a:
@dataclasses.dataclass(frozen=True)
class Account:
"""A valid account on the ledger."""
id: str # The ID of the account as a string.
Account.id: strID de string exclusivo da conta.
classe gcul.Amount
Representa um valor monetário denominado em uma moeda de contabilidade específica.
Instancie um valor usando
gcul.Amount(value: int, currency: gcul.Currency). A definição da
classe é aproximadamente equivalente a:
@dataclasses.dataclass(frozen=True)
class Amount:
"""Represents an amount denominated in a ledger currency."""
value: int
currency: Currency
classe gcul.Currency
Representa uma moeda totalmente qualificada no livro-razão. A definição da classe é aproximadamente equivalente a:
@dataclasses.dataclass(frozen=True)
class Currency:
"""Represents a fully qualified currency on the ledger."""
code: str
operator_id: str
classe gcul.ContractInterface
A classe de base para declarar a interface pública de um contrato.
Os métodos e as propriedades da classe de base gcul.ContractInterface não podem ser sobrepostos em subclasses. Além disso, os métodos na declaração da interface
não podem incluir nenhum código, apenas
... é permitido.
Para que um contrato chame métodos em outro contrato implantado no livro,
declare uma classe de interface que herde de gcul.ContractInterface. Os stubs de método precisam corresponder à declaração correspondente no contrato de destino. Isso
inclui argumentos, tipo de retorno e anotações de decorador, como
@gcul.readonly ou @gcul.payable.
No código do contrato, use self.call(target, payment) para
invocar um método de outro contrato.
O exemplo a seguir ilustra como um contrato TokenVendor interage
com outro contrato que implementa o ERC20Interface. Ele usa
self.call() para transferir tokens para um comprador em troca de um pagamento em moeda
no livro:
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)
Os tipos de interface também podem ser usados como campos de contrato, não apenas como parâmetros de método. Armazenar uma interface em um campo permite que um contrato mantenha uma referência a
outro contrato em todas as transações e substitua essa referência mais tarde. O
exemplo de contrato a seguir usa o ERC20Interface declarado anteriormente:
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
classe gcul.AccountField
Um tipo genérico integrado para declarar campos de conta. gcul.AccountField[T]
contém um valor separado do tipo T para cada conta que concedeu ao
contrato a permissão de armazenamento. Ele se comporta como um mapeamento com chaves gcul.Account,
que podem ser usadas para ler ou gravar um valor para cada conta específica.
Exemplo:
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 precisa ser um tipo que tenha um valor padrão. Portanto, gcul.Account, gcul.Amount,
gcul.Currency e tipos de interface não podem ser usados como valores de campo
de conta. Quando uma conta concede a permissão de armazenamento a um contrato, todos os campos da conta são preenchidos com o valor padrão do tipo para essa conta.
Restrição: a indexação com uma conta é a única operação que um campo de conta aceita. O campo em si não pode ser atribuído a uma variável, transmitido como um argumento, retornado de uma função ou iterado.
Restrição: não é possível excluir as entradas do campo de conta. Um valor armazenado pode ser substituído, mas a entrada em si é permanente.
Restrição: os campos de conta para gcul.sender, gcul.caller e qualquer
gcul.Account transmitido como um argumento de método (incluindo listas internas ou outros
contêineres) são carregados antes da execução do método. Para outras contas, uma única transação pode carregar lentamente os campos de conta para no máximo 15 pares (contract, account) distintos. O acesso a um campo de conta para o mesmo par várias vezes em uma transação conta apenas uma vez. Uma transação que excede esse limite falha.
@gcul.readonly
Anota um método de contrato como somente leitura. As declarações de método com esse decorador não podem alterar o estado do contrato nem fazer transferências de moeda no livro contábil. Eles
também não podem usar tipos de contêiner mutáveis nas assinaturas, por exemplo, list
ou dict. Em vez disso, eles precisam usar declarações de tipo somente leitura, como Sequence ou
Mapping.
@gcul.payable
Anota um método de contrato como exigindo um pagamento para ser executado. Se um método
espera receber um pagamento em moeda on-ledger usando gcul.payment, ele precisa
ser marcado com esse decorador. Caso contrário, a transação vai falhar.
gcul.sender: gcul.Account
Contém uma referência à conta que assinou e enviou a transação
InvokeContractMethod
atual. O valor de gcul.sender não muda durante a execução da transação, mesmo que ela envolva chamadas entre contratos, ao contrário de gcul.caller, que se refere ao autor da chamada imediato.
gcul.caller: gcul.Account
Contém uma referência ao autor da chamada imediato do método atual. Para transações diretas de usuários, gcul.caller corresponde a gcul.sender, que aponta para quem enviou a transação. Para chamadas entre contratos, gcul.caller corresponde ao
ID da conta do contrato de chamada. As chamadas internas em um contrato não alteram
o valor de gcul.caller.
gcul.payment: gcul.Amount
Mantém o pagamento transferido para um método decorado com @gcul.payable.
Acessar gcul.payment dentro de um método não pagável gera um erro de tempo de compilação.
Operadores
A maioria dos operadores disponíveis em Python é compatível com o GCULpy e funciona conforme o esperado.
- Adição (
+) e subtração (-), incluindo formas unárias e binárias. - Multiplicação (
*), divisão de piso (//) e módulo (%). - Exponenciação (
**) para expoentes positivos. - Comparações (
<,<=,>,>=,==,!=). - Bit a bit e (
&), ou (|), xor (^), deslocamento para a esquerda (<<), deslocamento para a direita (>>), negação (~). - Operações booleanas (
and,or,not). - Planejamento Identidade do objeto (
is). - Restrição: expoentes negativos não são permitidos e geram um erro de tempo de execução.
- Restrição: a divisão verdadeira (
/), por ter um tipo de retornofloat, não é permitida e resulta em um erro de tempo de compilação.
Fluxo de controle
A maioria das instruções de fluxo de controle do Python funciona no GCULpy com a mesma semântica:
pass.- chamadas de função interna (mesmo contrato, não recursivas).
assert.if ... then .. else ....for VAR in CONTAINER.- chamadas de função externa (para qualquer outro contrato, não recorrente).
- Instruções
breakecontinue. raise.- Declarações de roteiro
try ... except. - Declarações de roteiro
match. - Instruções
generatorseyielddo roteiro. - Gerenciadores de contexto do Roadmap e instruções
with.
Restrição: o GCULpy é intencionalmente Turing-incompleto para evitar loops infinitos e facilitar a análise estática. Veja como ele faz isso:
- Sem loops infinitos: a iteração só é permitida usando loops
forem contêineres finitos ou sequências limitadas comrange(...). Loopswhilenão são permitidos. Algumas atualizações de contêiner, como adicionar ou remover elementos de uma lista ou chaves de um dicionário, não são permitidas durante a iteração. - Sem recursão: uma função não pode chamar a si mesma, direta ou indiretamente. O ambiente de execução realiza verificações estáticas e de execução para detectar e rejeitar o uso de recursão.
- Sem fluxo de controle assíncrono: o uso de primitivos
asyncnão é permitido para manter os princípios básicos de design de previsibilidade, segurança e execução determinística. As operações assíncronas dificultam o raciocínio sobre o fluxo de controle de um programa, o que geralmente leva a vulnerabilidades e disputas.
Funções integradas.
O GCULpy é compatível com as seguintes funções integradas:
abs(x)Retorna o valor absoluto de um número inteiro
x. Os argumentos precisam ser do tipoint.len(s)Retorna o comprimento, ou seja, o número de itens, de uma string ou de um contêiner finito.
max(iterable)
max(arg1, arg2, *args)Retorna o maior item em um iterável ou o maior de dois ou mais argumentos. Chamar
max()em um iterável vazio gera um erro de tempo de execução.min(iterable)
min(arg1, arg2, *args)Retorna o menor item em um iterável ou o menor de dois ou mais argumentos. Chamar
min()em um iterável vazio gera um erro de tempo de execução.range(stop)
range(start, stop[, step=1])Gera uma sequência aritmética de números inteiros com um número inteiro diferente de zero
step.Restrição: só pode ser usado inline com loops
for, por exemplo,for i in range(10): .... Os objetos de intervalo não podem ser atribuídos a variáveis ou armazenados em campos, transmitidos como argumentos para funções ou consultados comlen().
Roteiro As seguintes funções integradas estão no roteiro de implementação:
|
A
B
C
D |
E
F
H
I
L |
O
P
R |
S
T
Z |
Notas de lançamento
3 de novembro de 2026. Lançamos a versão em disponibilidade geral do compilador
gculpyc(v0.2.0), incluindo suporte para chamadas entre contratos, transferências e pagamentos de moedas no livro, métodos@gcul.readonly,list, outras instruções de fluxo de controle e funções integradas.28 de janeiro de 2026. Versão inicial do compilador
gculpycdisponibilizada para os participantes do pré-lançamento particular do Universal Ledger. Para ver um tutorial sobre como usar o compilador, consulte Implantar um contrato programável.