見出し画像

StarkNet入門編|アーキテクチャやCairoについて

この記事は「Practical StarkNet lessons learned/著:Ted Suzman」を日本語翻訳したものです。



このガイドは、StarkNetをすでに使用したことがある人にとっておそらく最も役立つでしょう。


私は過去3〜4週間、StarkNetでプロジェクトをフルタイムで実装してきましたが、これには学んだ教訓の一部が含まれています。質問や提案:ツイッター@RoboTeddyで私にpingしてください。

StarkNetを初めて使用する人に向けて



・StarkNetを使用するために、STARKがどのように機能するかを理解する必要はありません。



・Starkwareチームの努力のおかげで、Cairo(プログラミング言語)とStarkNet(Cairoで記述されたプログラムをデプロイする場所)を学び、使用することができます。



・学ぶための最良の方法は、StarkNetのドキュメントに従うことです。カイロのドキュメントを使用し、カイロのplaygroundで演習を完了することで、必要なカイロを学ぶことができます。


・最終的には、より高度なカイロのしくみを読みたいと思うかもしれませんが、コードを読まなくても間違いなく書き始めることができます。



StarkNetアーキテクチャミニ入門書



(注:ロールアップがどのように機能するかについてのより完全な説明は、ここでは範囲外です。そのためには、Vitalikのガイドを試してください。)

StarkNetでは、tx(コントラクトデプロイメント、コントラクトコールなど)をシーケンサーに送信します。このシーケンサーは現在集中型でクローズドソースですが、後で分散型でオープンソースになります。


シーケンサーはtxのバッチを実行し、次の2つを生成します。


1 トランザクションバッチによって引き起こされたステートデルタのリスト(例:[「ストレージセル5を値10に更新」、「ストレージセル9を値12345に更新」])



2 以前のStarkNetステートに対して忠実に実行された場合、項目(1)にリストされたステートデルタをもたらす一連のトランザクションが存在するという証拠



特に、StarkNetトランザクション自体がチェーン上で記録されることは決してありません。(システムが安全に動作するために、実際には公開する必要はまったくありません!)。



・StarkNetでは、トランザクションはオフチェーンであり、結果のステートはL1 calldataにオンチェーンで保存されます。



・たとえばArbitrumでは、比較すると、トランザクションはL1 calldataにオンチェーンで格納され、結果のステートはオフチェーンで計算されます。



マキシム:計算は安いです。書き込みは高価です。


StarkNetはtestnetに料金がかかりません。メインネットの料金は、少なくとも最初はethで請求されます。料金は最初は単純で不正確かもしれませんが、StarkNetを実行するための基本的なコストを反映するように時間とともに進化する可能性があります。


Cheap things:


トランザクションcalldata。大きなtxを持っていてもまったく問題ありません!上記のように、これらはチェーン上に保存されません。



計算(加算、乗算、関数の呼び出しなど)—これはすべてバッチでオフチェーンで行われ、オンチェーンで比較的安価に検証されます。



ストレージ変数からの読み取り—これはオフチェーンで発生します。



Expensive things:



ストレージ変数の変更:これらの変更はL1 calldataに書き込む必要があり、コストがかかります。



コードを書いている間、私はステートの変更を除いてすべてを(理由の範囲内で)〜freeと考える傾向があります。


特定の種類の書き込みのコスト



書き込みは高価であるため、書き込みにかかる費用を見積もることができることが重要です。


・単一のstorage_varスロットを変更する基本的な書き込み



次のようにストレージ変数を定義するとします。


@storage_var
func _balances(addr: felt) -> (res: felt):
end


実行にかかる可能性のある多くのことを調べてみましょう。


_balances.write(1, 123456)


基本コスト


単一のストレージスロットの作成には、最大$ 0.60のコストがかかります(2021年11月現在)。



・書き込みにより、64バイトのステート差分が発生します(スロットインデックス番号の場合は32バイト、スロットストレージ値の場合は32バイト)。



・L1 calldataのコストは16ガス/バイトです



・ガス価格は〜130 gwei(2021年11月)


・Eth価格は〜$ 4200(2021年11月)



・64 bytes * 16 gas/byte * 130e-9 eth/gas * $4200/eth = ~$0.60



・これはまだイーサリアムストアよりも20倍安いです



・「Batching rebate」:特定のストレージスロットがn単一のStarkNetバッチ内の時間(約1〜4時間)に書き込まれる場合、各書き込みのコストはその分だけ1/nです。これは、ストレージスロットへの書き込みが、異なるtx、他のコントラクトからの呼び出しなどによって引き起こされた場合でも当てはまります。



「Compression rebate」:保存している値が一般的なものである場合、圧縮率が高くなり、calldataの使用量が少し少なくなる可能性があります。StarkNetがこれらの節約をあなたに還元するのは複雑かもしれないので、私はそれがすぐに起こることに依存しません。



ストレージに構造体を書き込むコスト



ストレージ変数に構造体を書き込むことは可能であり、そうするのに役立つことがよくあります。


struct Account:
   member id: felt
   member username: felt
   member karma: felt
end

@storage_var
func _accounts(addr: felt) -> (res: Account):
end

# ...

_accounts.write(2, Account(id=3, username=23434, karma=1000))


n個のメンバーを持つ構造体の書き込みは、単一のストレージスロットを変更する基本的な書き込みの約n倍のコストがかかります。


複合キーを持つストレージ変数への書き込みコスト



複合キーを持つストレージ変数を作成することができます。


例:

@storage_var
func _profiles(world: felt, country: felt, user_id: felt) -> (res: felt):
end

# ...

_profiles.write(10, 1, 1234, 100)

この場合、キーは(10、1、1234)で、書き込まれる値は100です。StarkNet(h / t Tom Brand)内で、これは


storage.write(key = hash(hash(10、1 )、1234)、value = 100)


—つまり、余分なハッシュのために少し多くの計算が必要ですが、それでも単一のストレージ値を変更するだけです。 1回の基本的な書き込みとほぼ同じコストです。



書き込みを回避するために追加の計算を使用する



格言を覚えておいてください。計算は安価で、書き込みは高価です。


StarkNetにRedditを実装していて、人々が提出物に何度も賛成するのを防ぎたいと想像してみてください。オプション:


1 すべての(user_id, submission_id)ペアをチェーンに保管します(高すぎます!)



2 アプリケーション全体のブルームフィルターを保存します。誰かが提出の増分に賛成しようとすると、賛成カウンターはブルームフィルターにまだ入っていない場合hash(user_id, submission_id)に限ります。



2番目のオプションは確率的です(賛成票は完全には追跡されません)が、はるかに安価になる可能性があります:各バッチで何度も更新される単一の小さなフェルトセット(ブルームフィルターを格納するためのもの)があり、大きなバッチ処理になりますリベート。


より安い書き込みが間近に迫っています



Starkwareは、ストレージの書き込みをはるかに安価にする可能性のある検証/ボリューションオプションに懸命に取り組んでいるようです(実装によっては、検閲への抵抗と活性の保証が犠牲になる可能性があります)。



カイロのコードを読んで学ぶ



・Cairo標準ライブラリ— Cairoを学び、使用できるライブラリ関数を学ぶためにこれを読んでください(それらはまだ他の場所で十分に文書化されていません)



・starknet-dai-bridgeは、小さいながらも高品質の例です。また、L1とL2の両方に展開する興味深い展開スクリプトもあります。



・OpenZepplinStarkNetコントラクト



・アージェントウォレットスタークネットコントラクト



StarkNet OS:本当の専門家によって書かれた多くのカイロコードの例!これは、StarkNet自体を実装するCairoコードです。コントラクトの発動などを処理します。



StarkNet / Cairoのデザインパターンと言語のトリック



ブール式



カイロには、次のようなブール式が組み込まれていません。 

x && yまたはp || NS


しかし、代わりに使用できるいくつかのトリックがあります。 


xとyがそれぞれ0または1であることがわかっているとします。次に… 


assert x || y -> assert (x - 1) * (y - 1) = 0
assert !x || !y -> assert x * y = 0
assert x && y -> assert x + y = 2
etc


これらの小さなトリックは、アサーション(例のように)、または他の式、ifステートメントの述語などで使用できます。


配列を保存できます



キーを使用してを定義し、それを使用して配列を格納できますstorage_var。例えば:


@storage_var
func _my_array(i : felt) -> (res : felt):
end
# ...
_my_array.write(0, 123)
_my_array.write(1, 456)
_my_array.write(2, 789)
# ...



Struct enum pattern



カイロには列挙型がありませんが、構造体を列挙型として悪用することができます。たとえば、この構造体を定義する場合:


struct DirectionEnum:
   member north: felt
   member south: felt
   member west: felt
   member east: felt
end



あなたはそれを発見するでしょうDirectionEnum.north == 0、DirectionEnum.south == 1、DirectionEnum.west == 2、とDirectionEnum.east == 3。


これは、構造体のそのメンバーのメモリオフセットを返すために機能しStruct.member_nameます。各フェルトのサイズは1であるため、後続の各メンバーは一意の増分値になります。


オプションのデータフィールドを効率的に保存する



ストレージに入れたいデータ(ユーザーのアカウントなど)がたくさんある場合があります。そのうちのいくつかのメンバーフィールドはオプションであるか、すぐに使用する必要はありません。


たとえば、ENSを実装していて、保存したいレコードがたくさんあるとします。


name
url
description
avatar
keywords
twitter
reddit



これらのフィールドの多くはオプションであり、多くのドメインでは設定されない場合があることに注意してください。


高価な方法



1つのアプローチは、構造体を作成し、すべてをストレージに保存することです。例えば:


struct Domain:
   member name: felt
   member url: felt
   # ...
   twitter: felt
   reddit: felt
end
@storage_var
func _domains(addr: felt) -> (res : Domain)
end
# ...
let domain = Domain(
   name=34523,
   url=234234,
   #...
   twitter=0,
   reddit=0,
)
_domains.write(addr, domain)
​


それはのようなスロットを含むストレージスロットのトン、書き込みを必要とするので、これは高価であり、twitterそしてreddit(これまでであれば)今設定する必要はありません。


安価な方法:storage enum struct



次のように列挙型とストレージ変数を定義します。

struct DomainStorageEnum:
   member name: felt
   member url: felt
   # ...
   member twitter: felt
   member reddit: felt
end
@storage_var
func _domains(addr: felt, storage_index : felt) -> (res : felt):
end


そして、次のように値を少しずつ書くことができます。


_domains.write(addr, DomainStorageEnum.name, 34523)
_domains.write(addr, DomainStorageEnum.url, 234234)



これにはもう少し計算が必要です(より長いマークルパスを証明する必要があります)が、必要に応じて値の束を初期化しないままにしておくことができるという利点があります。覚えておいてください:書き込みは高価な部分です!


Function pointers



get_label_addressを使用して関数へのポインターを取得してから、呼び出して関数を呼び出すことができます。


例 (h / t Martriay)

Functions not marked @view or @external are internal helpers


装飾されていない関数は、トランザクションやその他のコントラクトから呼び出したり呼び出したりすることはできません。これらは、コントラクトの内部にある純粋なヘルパー関数です。


アカウントの抽象化



アカウントコントラクト



イーサリアムL1には、パブリック/プライベートキーペアに基づいており、署名の検証、値の送信、コントラクトの呼び出しが可能な組み込みの「アカウント」があります。


StarkNetにはそのようなものは組み込まれていません。代わりに、署名を検証して他のコントラクトを呼び出す機能を持つコントラクトをStarkNetに明示的にデプロイすることにより、この機能を作成します。


たとえば、誰かが次のような「アカウント」コントラクトを展開する場合があります。


%lang starknet
%builtins pedersen range_check ecdsa
from starkware.cairo.common.hash import hash2
from starkware.cairo.common.registers import get_fp_and_pc
from starkware.cairo.common.cairo_builtins import HashBuiltin, SignatureBuiltin
from starkware.starknet.common.syscalls import call_contract, get_caller_address, get_tx_signature
@storage_var
func _public_key() -> (res: felt):
end
@constructor
func constructor(_public_key: felt):
   _public_key.write(_public_key)
   return ()
end
@external
func execute{pedersen_ptr : HashBuiltin*, syscall_ptr : felt*, range_check_ptr}(
       to : felt, selector : felt, calldata_len : felt, calldata : felt*)
   # 1. Verify that whoever invoked `execute` signed everything
   #    with the right private key
   let (hash) = hash_message(to, selector, calldata_len, calldata)
   let (signature_len, signature) = get_tx_signature()
   is_valid_signature(hash, signature_len, signature)
   #2. Call the contract that the account owner wants to interact with
   call_contract(
       contract_address=to,
       function_selector=selector,
       calldata_size=calldata_len,
       calldata=calldata)
end



アカウントコントラクトがデプロイされると、特定の公開鍵がコンストラクターに含まれます。誰かが電話をかけたいときはいつでもexecute、関連付けられた秘密鍵からの署名を含める必要があります(そうでない場合、txは拒否されます)。


このようにして、パブリック/プライベートキーペアによるアクセスをゲートすることが可能です—組み込みされていないことを除いて、イーサリアムと同じです—私たちはコントラクトコードを使用して自分たちでそれを行いました。


アカウントコントラクトを呼び出す具体的な例



StarkNetにアカウントコントラクトがデプロイされていて、それを使用してERC20コントラクトの伝達関数を呼び出すとします。


ERC20のtransfer機能は次のようになります。


func transfer(recipient: felt, amount: felt):
   let (caller_address) = get_caller_address()
   _balances.write(caller_address, ...)
   # ...
end


これから行うことは、アカウントコントラクトのexecute関数を呼び出し、transfer関数を呼び出すように指示することです。直接callするのではないことに注意してください。transfer代わりに、アカウントコントラクトにあなたに代わって何を電話するかを伝えています。


アカウントコントラクトを呼び出して、ERC20コントラクトの転送関数を呼び出して、たとえば30トークンを受信者に転送する方法は次の0x567RecipientAddr89とおりです。


starknet invoke \
   --address 0x1234AccountAddress5678 \
   --abi account_contract_abi.json \
   --function execute \
   --inputs \
       0x12ERC20Address34 \
       23267048542 \
       2 \
       0x567RecipientAddr89 \
       30



何が起こるかは次のとおりです。


・アカウントコントラクトのexecute機能が呼び出されます。



・このexecuteメソッドは、署名を検証してから、渡された入力を調べて以下を判別します。



どのコントラクトを呼び出す必要がありますか:to引数は0x12ERC20Address34


そのコントラクトのどの関数を呼び出す必要がありますか:この場合selectorはarg23267048542です。(これは文字列のハッシュです"transfer")


渡す必要のある引数の数:calldata_len、2この場合は、関数が2つの引数を取るためtransferです。


渡す必要のある実際の引数:calldata[ 0x567RecipientAddr89、]に設定されている引数30—受信者と転送される金額。



3  transfer関数が呼び出され、get_caller_addressを使用して、それを呼び出したコントラクトのアドレス(つまり、最初に呼び出したアカウントコントラクトのアドレス)が学習されます。


というよりむしろ「引数0x567RecipientAddr89および30を使用して転送を呼び出す」ではなく、「引数0x567RecipientAddr89および30を使用して転送を呼び出すようにアカウントコントラクトに指示します」です。


つまり、アカウントコントラクトは引数を受け取り、それらを検証してから、いくつかの引数を取得して、目的のコントラクトに渡します。


呼び出されたコントラクトは、いつでも誰がコールしたかを確認するために使用できます。


get_caller_address()


このようにして、アカウントコントラクトのアドレスは、イーサリアムのパブリックアドレスと同様に安定した識別子になります。


アカウントの抽象化は、簡単に言えば、ちょっと混乱する可能性があるので、すぐに理解しなくても心配しないでください。


詳細については、アカウントコントラクトの標準インターフェースとOpenZepplinの実装例に関するこのディスカッションを確認してください。

落とし穴と現在の制限



初期化されていないメモリと値の間のあいまいさ 0



デフォルトでは、storage_varこれまでに書き込まれたことがないキーからキーを読み取ると、返される値はになります0。したがって、ゼロは初期化されていないストレージスペースを意味する場合があります。


アプリケーションのロジックによっては、0ストレージから読み取りを行い、ストレージが(a)初期化されていないのか、(b)以前に実際に値が書き込まれたのかを確認できないシナリオが発生する可能性があります。0それ。


このあいまいさを回避するには、別のストレージスロットに依存して、対象のストレージスロットがすでに初期化されているかどうかを通知するか、値の書き込みを避けて、値を初期化されていないストレージの明確なマーカーの0ままにし0ます。


取り消された参照



ジャンプとifは参照を取り消すことができます。あなたは一般的にもっと多くのものを作ることによってこれを解決することができlocalます。このような場合に内部で何が起こっているのかを理解したい場合は、取り消された参照に関するセクションがある、より高度なHow CairoWorksガイドを読む価値があるかもしれません。


フェルトの最大サイズ



フェルトは251ビットに安全にフィットします。これは、32バイトではなく、31バイトに適合します。


@viewとマークされた関数を呼び出すことができます



マークされた関数とマークされた関数は@external、@view現在同じように動作します。つまり、ステートを変化させる関数を記述して、@view誰かがそれを呼び出すことができます。


複数のトランザクションが同じハッシュを持つことができます


現在、同じtxを複数回送信することが可能です(ただし、特定のウォレットコントラクトにはこれを防ぐナンスがあります)。


ゲートウェイは、指定されたハッシュを使用して最初のtxについてのみ応答します。


したがって、たとえば:


1 成功するtxAを送信します
2 あなたはそのハッシュによってtxをポーリングし、最終的にPENDING(別名、成功)を取り戻します
3 同一のtxBを送信しましたが、失敗しました
4 ハッシュによってtxをポーリングし、PENDING(別名、成功)を取り戻します— tx Bが失敗したという事実にもかかわらず!



tx Bについて学習するためにポーリングしたとき、ゲートウェイは実際にtx Aについて通知しました。言い換えると、ゲートウェイに重複したtxの運命について通知させる直接的な方法はありません。


Starkwareは、txの重複を防ぐためにプロトコルの変更を計画しています。


拒否されたtxは、Voyagerブロックエクスプローラーに表示されません。



これはある時点で変更される可能性がありますが、すぐには変更されません。


シンボリックなスタックトレースを取得するのは少し注意が必要です



次のようなスタックトレースをリクエストできます。


starknet tx_status \
   --hash "0xsometxhashgoeshere" \
   --contract starknet-artifacts/contract.cairo/contract.json
   --error_message



人間が読める名前で記号化されるスタックトレースの唯一の部分は、-contractを介して渡したコンパイル済みのコントラクト定義を含む部分です。


スタックトレースのさまざまなセクションを象徴するために、さまざまなコンパイル済みコントラクトを渡す必要がある場合があります。


invokeを使用してリターンデータを取得することはできません



うまくいけば、これは将来変わるかもしれません!それまでの間、最初にinvoke()、次にcall()必要なデータを取得することができます。


現在のタイムスタンプを取得するためのシステムコールはまだありません



StarkwareのTODOリストの上位にあるので、これがすぐに変更されることを願っています。


それまでの間、タイムスタンプstorage_varを作成/読み取り/書き込みすることで、simulate/stubを実行できます。


タイムスタンプシステムコールが実装されると、最初はStarkwareのシーケンサーを信頼する必要があります。最終的に、syscallによって返されるタイムスタンプは、信頼が最小化されます(たとえば、Ethereumのブロックタイムスタンプによって何らかの形で制限されます)。


まだすべてがオープンソースというわけではありません



Starkwareは次のことを約束しました。


・シーケンサーのオープンソーシングと分散化



・証明者のソースを作成する-生成する証明を特定のオンチェーンオープンソース検証者コントラクトに提出することを要求するライセンスの下で利用可能にします。



ベリファイアはすでにオープンソースです。


メインネットに接続すると、L2-> L1メッセージの送信に最大4時間の遅延が発生する可能性があります



StarkNetがより大規模に動作するようになると(そして、より頻繁なバッチをチェーン上に置くことは経済的です)、この待ち時間はいくらか低下する可能性があります。(一方、4時間は、optimistic rollupsに必要な7日よりもはるかに短いです。)


IAccount、IERC20などの重要なインターフェースは流動的です


Starkwareとコミュニティは通常、これらがどのように見えるべきかを理解するために協力しています。


イーサリアムの署名検証は間もなく開始されます



イーサリアムの署名検証はまだStarkNetに実装されていませんが、将来的に実装される予定です。実装されると、既存のイーサリアムのpub / privキーペアによって直接制御されるアカウントコントラクトを結ぶことが可能になります。


Starkwareは、アドレスがイーサリアムL1パブリックアドレスと一致するStarkNetアカウントコントラクトを作成することも可能にする場合があります。これにより、資金の回収などが可能になります。


Argent StarkNetウォレットブラウザ拡張機能α


Argentは、アルファ版のStarkNetウォレットブラウザ拡張機能を作成しました— Argentに感謝します!


starknet-hardhat-plugin



starknet-devnet


starknet-devnet —これによりローカルdevnetを作成できます。L1 <–> L2メッセージはまだサポートされていないことに注意してください。

Visual Studio Codeには、cairo-formatを使用してコードをフォーマットできるcairo拡張機能があります


StarkNetの実践に向けて



StarkNetのドキュメントに記載されているように、pytestを使用することをお勧めします。理論的根拠:


・カイロの新しいバージョンがリリースされても安定しています


・完全なスタックトレースを簡単に確認できます


・これは、starknet-devnetに対して、またはgoerlitestnetに対して直接実行されるテストよりも高速に実行されます。


・それをスピードアップするための主要な機会があります(これらを文書化する機会がありませんでした—学びたい場合は私にpingしてください!):



pytestのキャッシングフレームワークとdillを使用したキャッシングフィクスチャ


StarknetStateコピーメソッドを使用する


pytest-xdistとの並列処理



これまでの全体的な経験



StarkNetチームは非常に協力的です。彼らはまた非常に才能があり、速く動いています。



人々は物事がどのように機能するべきかについて話し合い、エコシステムをサポートするオープンソースプロジェクトに貢献します。



カイロを学ぶのはそれほど難しいことではありません。以前にたくさんの言語を学んだことがある場合、本当に生産的になるまでに1〜2週間かかる場合があります。



StarkNet自体は初期段階です—プロトコル自体はまだ進化しています。


StarkNet、周囲のツール、およびエコシステム/標準の両方に、多くの荒削りな部分があります。これは楽しいこともあれば、イライラすることもあります。



とは言うものの、StarkNetは私が知っている唯一の場所であり、箱から出してすぐにチェーン上で(今のところテストネット上で)証明されるチューリング完全なコードを書くことができます。物事は急速に進んでおり、これは生態系の出現についての前向きな初期の兆候です。



エコシステムの開発を支援し、お互いの質問に答えてくれたコミュニティメンバーに感謝します(例:Martriay、perama、Sean、janek、Julien、corbtなど)


そして、質問に答えたり、改善を行ったり、一般的に支援してくれたすべてのStarkwareの人々に感謝します(例:Tom、Uri、Eli、Lior、guthl、FeedTheFedなど)


そして、私と協力してくれて+このドキュメントをレビューしてくれたKyleに感謝します!


この記事が気に入ったらサポートをしてみませんか?