はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、コントラクトを簡単に複製できるブループリントコントラクトを利用するための、バイトコードフォーマットを定義している規格であるERC5202についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なERCについてまとめています。

この記事では一部分でOpcodesの知識が必要になります。 もし興味がある方は以下の記事を参考にしてください。

概要

"ブループリント"コントラクトとは、初期化コード(initcode)がオンチェーンに保存されるコントラクトを表す標準のことです。 この初期化コードとは、コントラクトがブロックチェーン上でどのように機能するかを定義するコードのことで、基本的にコントラクトの「設計図」のようなものです。 ブループリントコントラクトを使うことで、開発者は一度コードをデプロイし、その後はそのブループリントを使って同様のコントラクトを簡単に生成できるようになります。 これは、新しいコントラクトの作成とデプロイをより効率的かつ迅速に行うことができるようにするための方法です。

この概念は、特にERC20やERC721などの標準化されたコントラクトで有用です。 開発者は、特定の機能やロールを持つコントラクトを一から作成する代わりに、既存のブループリントを使用して必要な機能を持つ新しいインスタンスを生成できます。 例えば、新しいトークンを作成する場合、開発者はERC20やERC721のブループリントを用いて、カスタムロジックやパラメータを加えたmintやtransferなどの関数を持つ新しいコントラクトを迅速にデプロイできます。

また、ブループリントコントラクトは、異なるコントラクト間でのやり取りを簡素化し、より安全で効率的なやり取りを可能にします。 一度標準化された方法でブループリントが確立されると、それを使用する全てのコントラクトは互換性を持ち、予期しない動作を避けることができます。

動機

コントラクトのデプロイサイズを減らすために、初期化コード(initcode)をオンチェーンに「ブループリント」コントラクトとして保存し、その後EXTCODECOPYを使用してinitcodeをメモリにコピーし、CREATEまたはCREATE2を呼び出すというパターンが有用です。 しかしながら、この方法には以下の問題があります。

外部のツールやインデクサーが、あるコントラクトが「通常の」ランタイムコントラクトなのか、「ブループリント」コントラクトなのかを判定するのは困難です。 バイトコード内のパターンを経験的に探索してinitcodeであるかを判断するのは、維持管理と正確性に問題を抱えます。

initcodeをバイト単位でオンチェーンに保存することは、正確性とセキュリティの問題を引き起こします。 EVMは実行可能コードと他のタイプのコードを区別するネイティブの方法を持っていないため、initcodeが明示的にACL(アクセス制御リスト)ルールを実装していない限り、誰でもそのような「ブループリント」コントラクトを呼び出して、initcodeを通常のランタイムコードとして直接実行できます。 これは特に、ブループリントコントラクトに保存されたinitcodeがストレージへの書き込みや外部コントラクトの呼び出しなどの副作用を持っている場合に問題です。 もしブループリントコントラクトに保存されたinitcodeがSELFDESTRUCTオペコードを実行すれば、ブループリントコントラクト自体が削除されてしまい、そのブループリントに依存している下流のデプロイヤーコントラクトの正確な動作を妨げる可能性があります。 このため、実行を防ぐためにブループリントコントラクトに特別な前文を付け加えることが望ましいです。

前文(preamble)とは、コントラクトや文書の最初の部分に配置される特定のコードやテキストのことです。 プログラミングやブロックチェーンのコンテキストでは、前文は通常、コードの残りの部分に関する情報をエンコードしたシーケンスや識別子を含みます。 ブループリントコントラクトの場合、前文はコントラクトが実行可能な通常のランタイムコードではなく、initcodeを含むブループリントであることを示すために使用されます。 この前文により、コントラクトが誤って実行されるのを防ぎ、セキュリティを向上させることができます。 特定のパターンやシーケンスが前文として使用され、コントラクトの意図とタイプを明確にするためのマーカーとして機能します。

仕様

ブループリントコントラクトは、前文として0xFE71<バージョンビット><エンコーディングビットの長さ>を使用しなければなりません。 バージョンには6ビットが割り当てられ、エンコーディングビットの長さには2ビットが割り当てられます。 最初のバージョンは0(0b000000)で始まり、バージョンは1ずつ増加します。 <エンコーディングビットの長さ>の値0b11は予約されています。 長さビットが0b11の場合、3番目のバイトは継続バイトと見なされます(つまり、バージョンをエンコードするために複数のバイトが必要です)。 マルチバイトバージョンの正確なエンコーディングは、将来のERCに委ねられています。

ブループリントコントラクトの前文で使用されるバージョンとエンコーディングの長さのビットについて具体的な例を用いて説明します。

  • バージョンビット

    • 前文には6ビットがバージョン番号を表すために割り当てられています。
    • バージョンは0から始まり(0b000000)、1ずつ増加します。
    • これは、バージョン0からバージョン63(0b111111)までの64個の異なるバージョンを表現することができることを意味します。
    • たとえば、バージョン5はバイナリで0b000101として表されます。
  • エンコーディングの長さ

    • エンコーディングの長さには2ビットが割り当てられており、これは3つの異なる値(0b00、0b01、0b10)を表すことができ、4つ目の値0b11は特別な意味を持ちます。
    • たとえば、エンコーディングの長さが0b01の場合、これは後続の特定のバイト数がデータの長さを表すことを意味します。
  • 予約されたビット(0b11)

    • 長さエンコーディングビットが0b11の場合、これは「継続バイト」があることを意味します。
    • つまり、バージョン番号をエンコードするためには複数のバイトが必要です。
    • これは、将来、より多くのバージョンが必要になった場合に拡張性を持たせるための仕組みです。
    • 将来のERCでマルチバイトバージョンの正確なエンコーディング方法が定められる予定です。

具体的な例

  • 仮にバージョン2(0b000010)のブループリントコントラクトを作成するとします。
  • そして、エンコーディングの長さが1バイト(0b01)であるとします。
  • この場合、前文は以下のようになります。
text
0xFE71 000010 01
  • これは、バージョン2を示し、後続の1バイトがデータの長さを表すことを意味します。

このようにバージョンとエンコーディングの長さを用いることで、ブループリントコントラクトの前文は、コントラクトのバージョンとデータ構造を明確に表現し、将来の拡張性を確保するための情報を提供します。

ブループリントコントラクトには、少なくとも1バイトのinitcodeが含まれていなければなりません。

ブループリントコントラクトは、バージョンバイトとinitcodeの間に任意のバイト(データまたはコード)を挿入することができます。 このような可変長データを使用する場合、前文は0xFE71<バージョンビット><エンコーディングビットの長さ><バイトの長さ><データ>でなければなりません。 <エンコーディングビットの長さ>は、<バイトの長さ>が何バイトを取るかを表す0から2(含む)までの数を表し、<バイトの長さ>は<データ>が何バイトを取るかをビッグエンディアンエンコーディングした数です。

ブループリントコントラクトの前文には、特定のフォーマットが要求されます。 このフォーマットはコントラクトが正しく認識され、使用されるようにするためのものです。

例えば、バージョン1(0b000001)のブループリントコントラクトを作成し、10バイトのデータ(任意のバイトまたはコード)をinitcodeの前に挿入したいとします。 そして、この10バイトの長さをエンコードするために2バイト(16ビット)を使用することにしました。

  1. 前文の開始

    • すべてのブループリントコントラクトの前文は0xFE71で始まります。
  2. バージョンビット

    • このコントラクトのバージョンは1なので、バージョンビットは0b000001です。
  3. エンコーディングビットの長さ

    • 長さをエンコードするために2バイト(16ビット)を使用することにしました。
    • 2バイトは0b10としてエンコードされます。
  4. バイトの長さ

    • 挿入したいデータが10バイトであるため、この長さをビッグエンディアンでエンコードする必要があります。
    • 10はバイナリで0b0000000000001010となります。
    • これを2バイトで表すと、0x000Aとなります。
  5. データ

    • ここに10バイトのデータが続きます。
    • 例として、これを0x1234567890ABCDEFFFEDとします。

したがって、このブループリントコントラクトの完全な前文は以下のようになります。

text
0xFE71 000001 10 000A 1234567890ABCDEFFFED

これにより、ブループリントコントラクトは自身のバージョンを識別し、initcodeの前にどれだけのデータがあるかを知り、そしてそのデータが何であるかを正確に理解することができます。 これにより、ブループリントコントラクトはさまざまなサイズと内容のinitcodeを柔軟に扱うことが可能となります。

ビッグエンディアンエンコーディング ビッグエンディアンエンコーディングは、データをバイト列として表現する時に、最も重要なバイト(最上位バイト)を先頭に置く方法です。 これは数値やアドレスなどの複数バイトからなるデータを記憶装置に保存する時の一般的な方式の1つです。 ビッグエンディアンの対義語はリトルエンディアンで、こちらは最も重要でないバイト(最下位バイト)を先頭に置きます。

例として、数値0x12345678をビッグエンディアン形式でエンコードすることを考えましょう。

この数値を16進数で表すと、それぞれのバイトは次のようになります。

  • 0x12(最上位バイト)
  • 0x34
  • 0x56
  • 0x78(最下位バイト)

ビッグエンディアンエンコーディングでは、最上位バイトが最初に来るため、この数値は次のようにバイト列として表されます:

0x12 0x34 0x56 0x78

これに対して、もしリトルエンディアンエンコーディングを使用すると、最下位バイトが最初に来るため、バイト列は次のようになります:

0x78 0x56 0x34 0x12

ビッグエンディアンエンコーディングは、人間が読み書きする際に直感的であることが多く、ネットワーク上でデータを送受信する際の標準的な方法(ネットワークバイトオーダーとも呼ばれます)としても広く採用されています。

補足

ブループリントコントラクトは、ブロックチェーン上でコントラクトの初期化コード(initcode)を保存し、再利用するための仕組みです。 この仕組みを使うと、新しいコントラクトを作成する時にガスやストレージスペースを節約できます。 ただし、この効率性を実現するためには、ブループリントコントラクト自体の構造にいくつかの工夫が必要です。

まず、ブループリントコントラクトの前文は0xfeで始まります。 この0xfeはINVALIDオペコードであり、コントラクトが直接呼び出された場合には例外的な停止条件を引き起こし、実行を終了させます。 これは、ブループリントコントラクトが通常のコントラクトとして実行されるべきではないことを強制するための措置です。

しかし、0xfeで始まるコントラクトが他にも存在する可能性があるため、ブループリントコントラクトを明確に識別するためにマジックバイト0x71が続きます。 この0x71は、「blueprint」という文字列のkeccak256ハッシュ値の最後のバイトから取得されたもので、ブループリントコントラクトの前文として特別な意味を持ちます。

マジックバイト

マジックバイトとは、ブループリントコントラクトの前文に含まれる特定のバイトで、そのコントラクトがブループリントであることを識別するために使用されます。 このバイトはコントラクトが特定の種類または形式であることを示すために任意に選ばれ、他のランダムなデータや通常のコントラクトと区別するのに役立ちます。

具体例として、「blueprint」という文字列に対するkeccak256ハッシュ関数の最後のバイトを取ることで、マジックバイト0x71が選ばれました。 keccak256は一般的なハッシュ関数で、任意のデータに対して一意のハッシュ値を生成します。 この場合、「blueprint」という文字列をハッシュ化すると、ハッシュ値の最後のバイトが0x71になります。

たとえば、Pythonでこの操作を行うコードは以下のようになります。

python
import hashlib

# 'blueprint'文字列のkeccak256ハッシュ値を計算します。
hash_value = hashlib.sha3_256(b'blueprint').digest()

# ハッシュ値の最後のバイトを取得します。
magic_byte = hash_value[-1]

print(f"魔法のバイト: {magic_byte:02x}")

このコードを実行すると、0x71という値が「マジックバイト」として表示されます。 このようにして選ばれた0x71は、ブループリントコントラクトの前文で使われ、ブループリントコントラクトであることを明確に示します。 他の通常のコントラクトやランダムなデータが偶然にも0xFE71で始まる可能性は非常に低いため、この魔法のバイトはブループリントコントラクトの信頼性を高める重要な役割を果たします。

ブループリントコントラクトには少なくとも1バイトのinitcodeが必要であり、空のinitcodeは許可されていません。 これは、空のinitcodeが誤って設定されるという一般的な間違いを防ぐためです。

ユーザーが前文に任意のデータやコードを含めたい場合、可変長エンコーディングが使用されます。 これにより、データの長さが256バイト未満である場合、その長さを0バイトまたは1バイトで表すことができます。 長さを表すためのビットは、前文の3番目のバイトの2ビットに予約されています。

バージョンビットが含まれているため、将来的にブループリントコントラクトがアップグレードされる必要がある場合に対応できます。 もしバージョンビットを使い果たした場合に備えて、継続シーケンスも予約されています。 しかし、通常は2バイトで十分であり、特別な継続シーケンスマーカー(0b11)が用意されています。

最後に、Ethereum Object Format(EOF)が将来的にEVMに導入されることにより、ブループリントコントラクトをさらに明確に指定する新しい方法が提供されるかもしれません。 しかし、それまではこのERCを使用してブループリントコントラクトを標準化し、より効率的なブロックチェーンアプリケーションを実現することが可能です。

Ethereum Object Format(EOF) Ethereum Object Format(EOF)は、Ethereumのスマートコントラクトとその実行環境をより安全で効率的かつ透明にするために提案されている新しいフォーマットです。 現在のEthereum Virtual Machine (EVM) コードは単一のバイト列として格納されており、コントラクトの構造やコンテンツに関する明確な情報を提供しません。 これは、コントラクトの解析や検証を複雑にし、セキュリティリスクや効率の問題を引き起こす可能性があります。

EOFは、コントラクトのコードとデータを区別し、それぞれを異なるセクションに格納することで、これらの問題に対処しようとしています。 具体的には、EOFは以下のような特徴を持っています。

セクション化されたフォーマット コントラクトのコード、データ、および他の要素は、異なるセクションに分割されます。 これにより、コントラクトの構造がより明確になり、解析や検証が容易になります。

明確なバージョニング EOFにはバージョン番号が含まれており、異なるバージョンのフォーマットや実行環境の違いを明確に区別できます。 これにより、将来的なアップグレードや変更が容易になります。

セキュリティの向上 セクション化されたフォーマットにより、コードとデータを分離することができ、意図しないコード実行やデータ漏洩のリスクを低減できます。

効率的な実行 コントラクトの構造が明確になることで、EVMはより効率的にコードを解析し実行することができるようになります。 これは、ガス消費の削減にもつながります。

EOFは、Ethereumの将来的なアップグレードの一環として提案されており、Ethereum Improvement Proposals(EIP)を通じて議論されています。 EOFが実装されれば、Ethereumのスマートコントラクトのセキュリティと効率が大幅に向上することが期待されています。

互換性

互換性の問題は特にありません。

テスト

ブループリントコントラクトは、Ethereum上で再利用可能な初期化コード(initcode)を保存するための仕組みです。

text
0xFE710000
  • 0xFE71は、このコードがブループリントコントラクトであることを示すマジックヘッダです。
  • 0x00はバージョンが0であり、データセクションの長さが0バイトであることを示しています。
  • つまり、このコントラクトにはデータセクションがないということです。
  • 最後の0x00は、コントラクトの実行が終了するためのSTOP命令です。
text
0xFE710107FFFFFFFFFFFFFF00
  • 0xFE71は同じくマジックヘッダです。
  • 0x01はバージョンが0であり、データセクションの長さを示すビットが1バイトであることを意味します。
  • 0x07はデータセクションが7バイトであることを示しています。
    • この場合、それはFFFFFFFFFFFFFF、つまり0xFFが7回繰り返されたものです。
  • 最後に、0x00が続き、これはSTOP命令です。
  • 0xFE71|01|07|FFFFFFFFFFFFFF|00
text
0xFE71020100FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00
  • ここでも0xFE71はマジックヘッダを示します。
  • 0x02はバージョン0で、データ長を示すビットが2バイトあることを意味します。
  • 0100はデータセクションが256バイトであることを示しています。
    • ここではビッグエンディアン形式で長さがエンコードされているため、0100は256を意味します。
  • その後に256バイトのFFが続きます。
    • これは、0xFFが256回繰り返されたデータセクションです。
  • 最後に、0x00のSTOP命令があります。
  • 0xFE71|02|0100|FF...FF|00

これらの例は、ブループリントコントラクトの基本的な構造を示しています。マジックヘッダ(0xFE71)で始まり、バージョンとデータ長の情報、任意の長さのデータセクション、そしてSTOP命令で終わります。 データセクションはコントラクトの実行時に特定の目的のために使われる任意のデータを含めることができ、この柔軟性によりブループリントコントラクトは多様な用途に使用することが可能です。

実装

python
from typing import Optional, Tuple

def parse_blueprint_preamble(bytecode: bytes) -> Tuple[int, Optional[bytes], bytes]:
    """
    Given bytecode as a sequence of bytes, parse the blueprint preamble and
    deconstruct the bytecode into:
        the ERC version, preamble data and initcode.
    Raises an exception if the bytecode is not a valid blueprint contract
    according to this ERC.
    arguments:
        bytecode: a `bytes` object representing the bytecode
    returns:
        (version,
         None if <length encoding bits> is 0, otherwise the bytes of the data section,
         the bytes of the initcode,
        )
    """
    if bytecode[:2] != b"\xFE\x71":
        raise Exception("Not a blueprint!")

    erc_version = (bytecode[2] & 0b11111100) >> 2

    n_length_bytes = bytecode[2] & 0b11
    if n_length_bytes == 0b11:
        raise Exception("Reserved bits are set")

    data_length = int.from_bytes(bytecode[3:3 + n_length_bytes], byteorder="big")

    if n_length_bytes == 0:
        preamble_data = None
    else:
        data_start = 3 + n_length_bytes
        preamble_data = bytecode[data_start:data_start + data_length]

    initcode = bytecode[3 + n_length_bytes + data_length:]

    if len(initcode) == 0:
        raise Exception("Empty initcode!")

    return erc_version, preamble_data, initcode

このPython関数parse_blueprint_preambleは、ブループリントコントラクトのバイトコードを解析し、その構成要素を分解して返します。 具体的には、ERCバージョン、前文データ、そしてinitcode(初期化コード)を抽出します。 ここで、バイトコードはブループリントコントラクトのバイナリ表現です。

  1. バイトコードの検証

    • 最初に、バイトコードが\xFE\x71で始まっているかどうかを確認します。
    • これはブループリントコントラクトのマジックヘッダです。
    • もしこのヘッダがなければ、「Not a blueprint!」というエラーメッセージとともに例外を投げます。
  2. ERCバージョンの解析

    • バイトコードの3バイト目から、ERCバージョンを抽出します。
    • これは6ビットの値で、バイトの上位6ビットに格納されています。
    • バージョンは、そのバイトを右に2ビットシフトして、下位2ビットを切り捨てることで取得できます。
  3. 長さビットの解析

    • 同じく3バイト目の下位2ビットを使用して、データセクションの長さをエンコードするために使われるバイト数(n_length_bytes)を取得します。
    • もしこの値が予約された0b11であれば、例外を投げます。
  4. データセクションの長さの計算

    • 次のn_length_bytesバイトを読み取り、それをビッグエンディアン形式として解釈し、データセクションの長さ(data_length)を計算します。
  5. 前文データの抽出

    • n_length_bytesが0の場合、前文データはありません(None)。
    • そうでない場合、data_lengthの長さだけバイトコードからデータセクションを切り出して、preamble_dataとして保存します。
  6. initcodeの抽出

    • データセクションの後に続くすべてのバイトがinitcodeです。
    • これをinitcode変数に保存します。
    • もしinitcodeが空(長さが0)の場合は、例外を投げます。
    • ブループリントコントラクトには少なくとも1バイトのinitcodeが必要だからです。

最後に、関数は(erc_version, preamble_data, initcode)という形式のタプルでこれらの情報を返します。 これにより、呼び出し元はブループリントコントラクトの重要な要素にアクセスし、必要に応じてそれらをさらに処理できるようになります。 この関数は、ブループリントコントラクトのバイトコードがこのERCの仕様に従っているかどうかを検証し、解析するための強力なツールです。

以下の参考関数は、ブループリントに必要なinitcodeをパラメータとして受け取り、対応するブループリント契約(データセクションなし)をデプロイするEVMコードを返します。

python
def blueprint_deployer_bytecode(initcode: bytes) -> bytes:
    blueprint_preamble = b"\xFE\x71\x00"  # ERC5202 preamble
    blueprint_bytecode = blueprint_preamble + initcode

    # the length of the deployed code in bytes
    len_bytes = len(blueprint_bytecode).to_bytes(2, "big")

    # copy <blueprint_bytecode> to memory and `RETURN` it per EVM creation semantics
    # PUSH2 <len> RETURNDATASIZE DUP2 PUSH1 10 RETURNDATASIZE CODECOPY RETURN
    deploy_bytecode = b"\x61" + len_bytes + b"\x3d\x81\x60\x0a\x3d\x39\xf3"

    return deploy_bytecode + blueprint_bytecode

セキュリティ

このERCでは、ブループリントコントラクトとして特定されるための一意の前文0xFE71が提案されています。 しかし、理論上は、既にオンチェーン上に存在するコントラクトが偶然にも同じプレフィックスを持っている可能性があります。 つまり、これらのコントラクトはブループリントコントラクトではないにもかかわらず、前文が0xFE71で始まるかもしれません。 しかし、このような偶然の一致は深刻なリスクとは見なされていません。

理由は、このERCに基づいてブループリントコントラクトを識別するインデクサー(ブロックチェーン上のデータを整理・索引するシステム)は、単にコードのプレフィックスを見るだけではなく、ソースコードをコンパイルして前文を確認し、それが意図した通りのブループリントコントラクトであるかを検証します。 この検証プロセスにより、偶然の一致は無視され、実際にブループリントコントラクトとして設計されたものだけが認識されます。

さらに、2022年7月8日の時点でEthereumメインネットを調査した結果、0xFE71で始まるバイトコードを持つコントラクトは一つも見つかっていません。 これは、提案された前文が現在のところユニークであることを示しており、既存のコントラクトとの衝突の可能性が非常に低いことを意味します。 結果として、この新しい前文を採用することによるリスクは限定的であり、ブループリントコントラクトの識別と使用に関して安全性が保たれると考えられています。

引用

Charles Cooper (@charles-cooper), Edward Amor (@skellet0r), "ERC-5202: Blueprint contract format," Ethereum Improvement Proposals, no. 5202, June 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5202.

最後に

今回は「コントラクトを簡単に複製できるブループリントコントラクトを利用するための、バイトコードフォーマットを定義している規格であるERC5202」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!