こんにちは。なまはげです。
前回はコントラクトの流れを理解し、グローバル変数のセッティングやコントラクトのインターフェースをトレイトとして記述していきました。今回は第三回目、実際に書いていきます。
rustについては最低限の説明はする予定ですが、十分ではないと思われます。
書く前の前提知識
型がEthereum準拠の独自の型
まず、基本的なチェーンに記録したり読み出したりする際に使う型は、ほとんどがEnigmaライブラリ(eng_wasm::eng_pwasm_abi::eth::AbiType
)で独自に定義されている型です。独自に定義、と言ってもほとんどがethereum-typesで定義されている型そのままの利用です。
一例をあげると
U256
・・・ この型はSolidityのuint256に相当する型です。structとして表現されており、組み込みのchecked_add(self, other: U256)
などで四則演算を行います。
H160
・・・ 固定長のバイナリデータ。Solidityのbytesに相当しますが、アドレスなどもこのH160で表現します。
他にもStringなども独自の実装がなされており、普通のrustとはかなり異なった側面があります。
またどうやら浮動小数点(f64、f32)はインポートしても使えないようです。
全ての型の実装については詳細があまり自分もよくわかっておらず、わかり次第また記事にしようかと思います。
保存するデータはwrite_stateマクロで書き込み、read_stateマクロで読み出す
データの保存ですがwrite_state!(key => value)
と言う関数を利用して書き込みます。
Pythonの辞書やSolidityのmapping型のようなkeyとvalueが組み合わせになったデータの型です。
またread_state!(key)
で取り出すことができます。
前回、最初の方にグローバルな定数としていろんな&str
型を定義しましたが、これらはstateを書き込む際のkeyとして利用します。
また各NFTのくじは一番最初に作ったくじならLottery_0
と言うようにLottery_番号
の形をkeyとして書き込みます。
基本的にデータは秘匿され、返り値によってユーザーは中身を見ることができる
これが一番重要な点です。
コントラクトの計算内容というのはTEEの外部からは見れません。ではユーザーはどうやって自分の残高などの情報を確認するかというと関数の返り値で確認します。
返り値はユーザーの公開鍵で暗号化されて返されます。よって関数を実行した人しかその値を見ることができません。また、ユーザーもその返り値以外のデータは見れません。
なのでEnigmaコントラクト実装において 返り値の設定と関数へのアクセス制御は非常にセキュリティ的に重要です。
関数へのアクセス制御は署名を用いたverify関数を実装することで (参考)実現できそうですが、今回は行いません。(まだ僕もよくわかってない)
便利機能とプライベートな関数をかく
まず最初に便利機能を実装していきます。
fn create_lottery_key(lottery_num: U256) -> String {
let mut key = String::from(LOTTERY);
key.push_str(&lottery_num.to_string());
return key;
}
fn h160_to_string(address: H160) -> String {
let addr_str: String = address.to_hex();
return [String::from("0x"), addr_str].concat();
}
fn random_range(my_max: u16) -> u16 {
let entropy: u8 = Rand::gen();
let system_max: u8 = u8::max_value();
return (my_max * (entropy as u16)) / (system_max as u16);
}
これはコントラクトで呼び出す便利機能を実装したものです。
まずfn create_lottery_key(lottery_num: U256) -> String
はstateに書き込む際のkeyとなるLottery_番号
を生成するための関数です。くじのID(U256)を入力するとString型で返ってきます。
fn h160_to_string(address: H160) -> String
はaddressをH160型から"0x"をくっつけた文字列に変換してくれる関数です。ユーザーのアドレスを文字列で管理したいときに使います。
fn random_range(my_max: u16) -> u16
はmy_max以内(今回は参加者を選び出すため、最大値はくじの参加者の数)の乱数を返してくれるものです。乱数もコントラクト内に記述できるのは処理過程が見れないEnigmaならではです。
次にプライベートな関数を記述していきます。
これは外から呼び出せない関数でstruct Contract
に実装していきます。
(impl
を用いることで構造体に関数を持たせることができる)
impl Contract {
fn get_ownership() -> Ownership {
match read_state!(OWNERSHIP) {
Some(ownership) => ownership,
None => panic!("ownership should already exist"),
}
}
fn get_whitelist() -> HashSet<H160> {
match read_state!(WHITELIST) {
Some(whitelist) => whitelist,
None => HashSet::new(),
}
}
fn get_lotteries() -> U256 {
match read_state!(LOTTERIES) {
Some(lotteries) => lotteries,
None => U256::from(0),
}
}
fn get_lottery(lottery: &str) -> Lottery {
match read_state!(lottery) {
Some(lottery) => lottery,
None => panic!("lottery does not exist"),
}
}
}
全部stateから値を取ってきているだけです。
read_state!(key)
の返り値はOption型(値が存在すればSome(Value)とSome()でラッピングした値が返され、なければNoneが返ってくる)です。
今回はmatch節を利用して値が存在すれば値を返し、なければpanic!(エラーのようなもの)もしくはデフォルト値を返しています。
コントラクトコードを記述する
コントラクトコードは前回インターフェースを実装しましたが、その通りに実装していきます。
コード内のコメントで関数の説明を行い、コードの下で説明が必要なものについて詳細な説明を行なっていきます。
impl ContractInterface for Contract { //ContractInterfaceというtraitをContract構造体に実装
#[no_mangle]
fn construct(owner_addr: H160, deposit_addr: H160) -> () {//デプロイ時に呼ばれる関数。コントラクトのオーナーとEthereumコントラクトのアドレスをstateに記述している。
write_state!(OWNERSHIP => Ownership {
owner_addr: owner_addr,
deposit_addr: deposit_addr
});
}
#[no_mangle]
fn add_to_whitelist(addresses: Vec<H160>) -> () {//ホワイトリストに追加
let mut whitelist = Self::get_whitelist();//変更するためmutつける。write_state!までmoveはされないので参照でなくて良い
assert!((whitelist.len() + addresses.len()) <= MAX_PARTICIPANTS as usize);//人数が最大値を超えていなかったら追加(今回ブラックリストの概念はない)
whitelist.extend(addresses.iter());//リストに追加(HashSetに追加する)
write_state!(WHITELIST => whitelist);//その値を書き込む
}
#[no_mangle]
fn get_whitelist_size() -> U256 {
let whitelist = &Self::get_whitelist();//値の参照をとる(消費しない)ので&つける
return U256::from(whitelist.len());
}
#[no_mangle]
fn create_lottery(
contract_addr: H160,
token_id: U256,
max_participants: U256,
owner_addr: H160,
) -> U256 { //新しいアドレスの構造体を作ってstateに記述する
let ownership = &Self::get_ownership(); //これは値を消費したくないので&をつける
let lotteries = Self::get_lotteries(); //現在のくじの数を取得
let id = lotteries.checked_add(U256::from(1)).unwrap(); //”Lottery_現在のくじの数+1”という新しいIDを作成
let lottery = Lottery {
id: id,
contract_addr: contract_addr,
token_id: token_id,
participants: Vec::new(),
max_participants: max_participants,
winner: H160::zero(),
status: LotteryStatus::JOINING,//参加可能なのでJOININGに設定
};
//Ethereumコントラクトのインスタンスを作成してlotteryCreated関数を動かす
let deposit = EthContract::new(&h160_to_string(ownership.deposit_addr));
deposit.lotteryCreated(
id,
token_id,
max_participants,
contract_addr,
owner_addr
);
//くじを書き込む
write_state!(LOTTERIES => id, &create_lottery_key(id) => lottery);
return lotteries;
}
#[no_mangle]
fn get_lotteries_size() -> U256 {
return Self::get_lotteries();
}
#[no_mangle]
fn join_lottery(lottery_num: U256, address: H160) -> () { //くじに参加するための関数
let ownership = &Self::get_ownership();
let lottery_key = &create_lottery_key(lottery_num);
let mut lottery = Self::get_lottery(lottery_key);
let max_participants = U256::as_usize(&lottery.max_participants);
// 参加者が参加可能人数よりオーバーしていないか確かめる
assert!(
lottery.participants.len() < max_participants,
"max amount of lottery people"
);
// 二重登録していないか調べる
assert!(
!lottery.participants.contains(&address),//Vec<H160>にaddressが存在しないか調べる
"participant already exists"
);
// 新しい参加者を挿入
lottery.participants.push(address);
// もし参加可能人数上限に達したらフラッグをJOININGからREADYに変更
if lottery.participants.len() >= max_participants {
lottery.status = LotteryStatus::READY;
}
// Ethereumコントラクトに参加者を書き加える。
// この際バイナリではなく0xから始まるSolidityのアドレスの形式に直してあげる。
// またStatusはそのままだとSolidityに通じないため、usize(整数)にキャスト(変換)する
let deposit = EthContract::new(&h160_to_string(ownership.deposit_addr));
deposit.userJoined(lottery.id, U256::from(lottery.status as usize));
write_state!(lottery_key => lottery);
}
#[no_mangle]
fn get_lottery_info(lottery_num: U256) -> LotteryInfo {
let lottery_key = &create_lottery_key(lottery_num); // 値を消費しないよう&をつける
let lottery = Self::get_lottery(lottery_key); // 返り値にするだけなので値は消費されない
return (
lottery.id,
lottery.contract_addr,
lottery.token_id,
U256::from(lottery.participants.len()),
lottery.max_participants,
lottery.winner,
U256::from(lottery.status as u32),
);
}
#[no_mangle]
fn roll(lottery_num: U256) -> H160 { //くじ引きを実行する関数
let ownership = &Self::get_ownership();
let lottery_key = &create_lottery_key(lottery_num);
let mut lottery = Self::get_lottery(lottery_key);
assert!( // StatusがREADYじゃないと実行しない
lottery.status == LotteryStatus::READY,
"lottery is not ready to roll"
);
let index = random_range(lottery.participants.len() as u16);//くじ引きで勝者を決める
let winner = lottery.participants[index as usize];
lottery.status = LotteryStatus::COMPLETE; //完了にStatusを変更する
// update Deposit & release
let deposit = EthContract::new(&h160_to_string(ownership.deposit_addr)); //勝者に送金する
deposit.winnerSelected(lottery.id, winner);
write_state!(lottery_key => lottery);
return winner;
}
}
まず目につくのは#[no_mangle]
という各関数にくっついているやつです。
こいつはrust以外の環境下でコードを動かすのに必要なcrate_typeです。
全体の処理の流れは
1. デプロイ時のconstruct()関数でコントラクトオーナーを定義する
2. create_lottery()関数でくじを作成する
3. 参加者はjoin_lottery()関数でくじに参加する
4. 参加者が上限まで達したくじの状態をREADYに変更する
5. Roll()関数で勝者を決定してNFTを支払う。
次にEthereumコントラクトとの通信について説明します。
let eth_contract = EthContract::new(&String型かつ0x始まりに直したアドレス));
でコントラクトのインスタンスを作成します。
そこから
eth_contract.実行したい関数(引数)
で動かすことができます。
渡せる型がシークレットコントラクトの型と異なる(U256ではなくusize型を整数として利用するなど)ようなので注意
コンパイル
まず、Ethereumコントラクトのインターフェースを記述したjson(Truffleとかでコンパイルするとついてくるアレ)を/secret_contracts/lottery
内に入れる必要があります。
今回のEthereumコントラクトは至極普通のコントラクトなので説明は省きます。
また、jsonもわざわざ取得しません。とりあえずコピペしましょう(笑)
ここからdeposit.jsonを取得できます。
このファイルを/secret_contracts/lottery
におきましょう。
ここからdiscovery compile
でコンパイルできます。
デプロイおよびテストは次回やります!!!
多分まだできていない点
- ownerの認証をするためにroll関数ではownershipを呼び出していると思われるのですが、使われていないので未実装っぽい
- 乱数の生成はもっと工夫できそう