GCULpy 言語

GCULpy は、安全で読みやすく、監査可能になるように設計された、Python の厳密な静的型付きサブセットです。この設計では、一般的なスマート コントラクトの脆弱性を防ぎ、コントラクトの動作を常に予測できるようにするために、Python の特定の動的機能が意図的に制限されています。

このページでは、GCULpy 言語仕様のリファレンスを提供します。このリファレンスでは、ユニバーサル台帳ネットワーク上のコントラクトのコアコンセプトとライフサイクルについて説明します。

基本コンセプト

次のコントラクトは、GCULpy でサンプル ERC20 トークンを定義します。

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

GCULpy コントラクトは、gcul.Contract から継承するクラスです。これには、フィールド(状態を保存)とメソッド(フィールドで動作する処理ロジック)が含まれます。

フィールド

GCULpy 契約の状態はフィールドに保存されます。すべてのフィールドは、クラスレベルで静的型を使用して宣言する必要があります。フィールドには次の 2 種類があります。

  • 契約フィールドには、契約自体に保存された単一の値が保持されます。ERC20Token の例では、symbol: strtotal_supply: int は契約フィールドです。

  • アカウント フィールドには、契約に関与する各ユーザー アカウントの個別の値が保存されます。これらは常に、gcul.Account をキーとする辞書(dict)として宣言されます(balance: dict[gcul.Account, int] など)。コントラクトがユーザーのアカウントに書き込むには、ユーザーがコントラクトにストレージ権限を明示的に付与する必要があります。データが保存されると、変更または削除できるのは契約インスタンスのみになります。ユーザーは変更または削除できません。

メソッド

メソッドは、コントラクトの実行可能なロジックを定義します。これらは Python メソッドのように動作し、コントラクトのフィールドを読み取りまたは変更できます。

  • __init__: コンストラクタは、コントラクトが最初にデプロイされたときに 1 回だけ呼び出されます。これは、コントラクト フィールドの初期状態を設定するために使用されます。コンストラクタで値が割り当てられていないフィールドには、適切なデフォルト値が設定されます。たとえば、int フィールドには 0dict フィールドには空のディクショナリが設定されます。

  • プライベート メソッド: アンダースコアで始まるメソッド(_internal_logic など)はプライベートであり、同じコントラクト内の他のメソッドからのみ呼び出すことができます。Universal Ledger インタープリタは、この制約を適用します。

  • パブリック メソッド: アンダースコア(_)で始まらないメソッドはすべてパブリックです。公開メソッドは、ROLE_CONTRACT_PARTICIPANT を持つユーザーが InvokeContractMethod トランザクションを送信することで呼び出すことができます。

契約ライフサイクル

以降のセクションでは、GCULpy 契約のライフサイクルに関連する一般的なオペレーションについて説明します。

コントラクトをデプロイする

まず、gculpyc コンパイラを使用して GCULpy ソースコードをコンパイルします。ROLE_CONTRACT_CREATOR を持つユーザーは、CreateContract トランザクションを送信して、コンパイルされたバイトコードをユニバーサル台帳ネットワークにデプロイできます。詳細な手順については、プログラマブル コントラクトをデプロイするチュートリアルをご覧ください。

このようなトランザクションは次のようになります。

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

ネットワークがこのトランザクションを処理すると、次のようになります。

  • コンストラクタ(__init__ メソッド)が実行され、新しいコントラクト インスタンスが作成されます。
  • トランザクションの送信者がコントラクトの所有者になります。
  • コントラクト インスタンスは台帳に永続的に保存され、一意のコントラクト ID が割り当てられます。この ID はトランザクション出力の一部として返されます。

権限を付与する

コントラクトがユーザーに代わってアカウント フィールドにデータを保存するには、ユーザーがコントラクトに保存権限を付与する必要があります。これはセキュリティ上の重要な手順です。ROLE_CONTRACT_PARTICIPANT を持つユーザーは、特定のコントラクト ID の GrantContractPermissions トランザクションを送信できます。

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

ネットワークがこのトランザクションを処理すると、次のようになります。

  • コントラクトでアカウント フィールドが定義されていない場合、トランザクションは拒否されます。
  • コントラクトでアカウント フィールドが定義されている場合、それらはすべてデフォルト値(contract.balance[gcul.sender] = 0 など)で入力されます。これらの値は、アカウントのデータの一部としてワールド状態に保存され、トランザクション送信者はこの特定のコントラクト インスタンスに参加しているものとして登録されます。

コントラクト メソッドを呼び出す

コントラクトがデプロイされ、必要な権限が付与されると、ユーザーはパブリック メソッドを呼び出してコントラクトを操作できます。ROLE_CONTRACT_PARTICIPANT を持つユーザーは、コントラクト ID、メソッド名、引数値を指定して InvokeContractMethod トランザクションを送信できます。

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

ネットワークがこのトランザクションを処理すると、次のようになります。

  • 指定された CONTRACT_ID に関連付けられた契約インスタンスが取得されます。
  • メソッド mint(beneficiary=Account("BENEFICIARY_ID"), value=10) が実行されます。受取人の Account オブジェクトは、ランタイムによってビルドされ、検証されます。メソッドのロジックでは、指定された ID が有効で、台帳上の既存のアカウントを参照していることを安全に想定できます。
  • なんらかの理由でメソッドが失敗した場合、トランザクションは失敗し、コントラクトの状態は更新されません
  • メソッドが成功すると、コントラクトの更新された状態がワールド状態に記録されます。

言語仕様

GCULpy は安全性と予測可能性を重視して設計されているため、いくつかの Python 機能が禁止されています。これらの機能は 制限ラベルで示されます。これらの制限は、コントラクト ロジックの読み取り、監査、静的分析を容易にし、予期しない動作や安全でない動作を制限するために導入された、永続的な言語機能です。

他の機能は ロードマップ ラベルで示されています。これらは実装ロードマップにありますが、gculpyc コンパイラではまだサポートされていません。

GCULpy は、静的型付けを重視した一般的な変数型を幅広くサポートしています。

コアバリューのタイプ:

  • intboolstrNone はすでにサポートされています。
  • ロードマップ DecimalbytesEnum はロードマップに記載されています。
  • 制限 floatcomplex は使用できません。

コンテナタイプ:

  • dict はすでにサポートされています。
  • ロードマップ listtuplesetdataclass はロードマップに記載されています。
  • 制限事項 コンテナ内の値には、具体的な型を指定する必要があります。たとえば、dict[str, int] は許可されますが、プレーンな dictdict[str, Any] は許可されません。
  • コンテナのネストがサポートされています(例: dict[str, list[int]])。

制限事項: コントラクト フィールドとアカウント フィールド、関数パラメータと戻り値の型など、すべての変数は静的に定義され、型指定されている必要があります。型は実行時に変更できず、具体的な型のみがサポートされます。型は値として使用できません。たとえば、変数に格納したり、引数として関数に渡したりすることはできません。宣言されていないフィールドに値を割り当てようとすると、コンパイル時エラーが発生します。

クラスと継承

最初は、基底 gcul.Contract クラスの直接のサブクラスであるクラスのみを定義できます。この厳格なルールにより、完全な Python 継承の複雑さを回避できます。完全な Python 継承は、見つけにくいバグを発生させ、コードの推論を困難にする可能性があります。セキュリティのため、親クラスのプロパティまたはメソッドをオーバーライドしようとするとエラーが発生し、予期しない動作に対する明確な保護手段が提供されます。

ロードマップ GCULpy は、コア原則を維持しながら、より柔軟性を提供します。ロードマップには、@override デコレータで明示的に管理されるメソッドのオーバーライドを使用して、ユーザー定義クラスの単一継承のサポートが含まれています。また、直接的で予測可能なオペレーションを保証するため、super() 組み込みは引数なしの形式でのみサポートされます。

gcul モジュール

GCULpy には、コントラクト開発に必要な型と変数を含む組み込みの gcul モジュールが用意されています。

クラス gcul.Contract

すべてのコントラクトの基本クラス。直接インスタンスを作成することはできません。コントラクトは CreateContract トランザクションでのみインスタンス化されます。基本 gcul.Contract クラスのメソッドとプロパティは、サブクラスでオーバーライドできません。

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

    指定されたアカウントが契約所有者の場合は True を返します。

クラス gcul.Account

台帳上のユーザー アカウントを表す組み込み型。gcul.Account オブジェクトを直接作成することはできません。ランタイム環境がオブジェクトを作成し、関数またはメソッドの引数として提供します。アカウント ID をトランザクション引数として渡すと、ランタイムが自動的に検証します。登録済みアカウントの有効な ID の場合は、完全なアカウント オブジェクトに変換されます。そうでない場合、トランザクションは失敗します。これにより、常に有効なアカウントのみを使用できます。

このクラスの定義は、おおよそ次の定義と同等です。

@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

任意のメソッドで使用できる特別な変数。現在のトランザクションに署名して送信したアカウントへの参照を保持します。

ロードマップ: 契約とアカウントの管理と操作に関するデベロッパーの能力を向上させます。契約オブジェクトへの参照を引数として渡し、フィールドに保存して、一意の ID(contract.id: str)にアクセスできるようになります。同様に、アカウント オブジェクトへの参照を保存して、ID を取得できるようになります。

演算子

Python で使用できるほとんどの演算子は GCULpy でサポートされており、想定どおりに動作します。

  • 加算(+)と減算(-)。単項形式と二項形式を含む。
  • 乗算(*)、切り捨て除算(//)、剰余(%)。
  • 正の指数に対するべき乗(**)。
  • 比較(<<=>>===!=)。
  • ビット演算の AND(&)、OR(|)、XOR(^)、左シフト(<<)、右シフト(>>)、否定(~)。
  • ブール演算子(andornot)。
  • ロードマップ オブジェクト ID(is)。
  • 制限 負の指数は許可されず、ランタイム エラーが発生します。
  • 制限事項 float 戻り値の型を持つ真の除算(/)は許可されず、コンパイル時エラーが発生します。

制御フロー

Python のほとんどの制御フロー ステートメントは、同じセマンティクスで GCULpy で動作します。

  • pass ステートメント。
  • 内部関数呼び出し(同じコントラクト、非再帰)。
  • assert ステートメント。
  • if ... then .. else ... ステートメント。
  • for VAR in CONTAINER ステートメント。
  • ロードマップ外部関数呼び出し(他のコントラクトへの呼び出し、非再帰的)。
  • ロードマップ break ステートメントと continue ステートメント。
  • ロードマップ raise ステートメントと try ... except ステートメント。
  • ロードマップ match ステートメント。
  • ロードマップ generators ステートメントと yield ステートメント。
  • ロードマップ コンテキスト マネージャーと with ステートメント。

制限 GCULpy は、無限ループを防ぎ、静的分析を容易にし、予測可能なトランザクション処理コストを確保するために、意図的に チューリング不完全にしています。このポリシーは次のように適用されます。

  • 無限ループなし: 繰り返しは、有限コンテナに対する for ループのみが許可されます。while ループは許可されません。コンテナの更新(リストへの要素の追加や削除、辞書へのキーの追加など)は、コンテナの反復処理中は許可されません。
  • 再帰なし: 関数は、直接的または間接的に自身を呼び出すことはできません。ランタイム環境は、静的チェックとランタイム チェックの両方を実施して、再帰の使用を検出し、拒否します。
  • 非同期制御フローなし: 予測可能性、セキュリティ、決定論的実行というコア設計原則を維持するため、async プリミティブの使用は許可されていません。非同期オペレーションでは、プログラムの制御フローを推論することが難しく、脆弱性や競合状態につながることがよくあります。

組み込みの関数

ロードマップ: 信頼性の高い構築を可能にする、最も基本的な機能とよく使用される機能が組み込まれた関数のロードマップをご紹介します。

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

リリースノート