Substrate Essentials Vol.2 ~TCR on Substrate~

Yuya Sugano
43 min readMay 2, 2019

--

特定の企業もしくは開発グループが作成したブロックチェーンは、当然ながら、特定のコンセンサスアルゴリズムや仕様を持っています。DAppsを開発するという視点で見た場合、ブロックチェーンレイヤーのアーキテクチャに制約や制限がある場合が少なくありません。Substrateはテーラーメイドなブロックチェーンの開発を可能にするライブラリ群です。Vol.1ではSubstrateの概要の説明を行いました。この記事ではSubstrateでのTCR(Token Curated Registry)の実装サンプルの備忘録です。[1]

Substrateとは

ブロックチェーンを利用したDApps(Decentralized Applications)の開発においては、レイヤー1であるブロックチェーンのコア実装がアプリケーション開発に大きく影響します。

例としてEthereumのスマートコントラクトによるDAppsの実装では、TPS(Transaction per second)の低さによる処理の遅延や、段階的なハードフォークが、実用的なアプリケーション開発を遠ざけている原因の1つとして考えられています。TPSの対策としてはサイドチェーンを利用して処理をオフロードするPlasmaがレイヤー2技術として検討されています。Ethereumはまだ開発途中の段階であり、現在のConstantinopleからSharding やPoS(Proof of Stake)の実装を経てSerenityへ移行することでハードフォークは限定的になると考えられますが、アプリケーションがレイヤー1のブロックチェーンコアの技術仕様に依存している状況は変わりません。

クラシックなアプリケーションで喩えると、TCP/UDPに制約が存在し、開発のボトルネックになっている状況とも言えるかもしれません。従来のWebアプリケーションはTCPやUDPを使用して開発されています。Substrateはネットワーク上で自由にアプリケーションを構築するブロックチェーン基盤を作成できるプラットフォーム技術のようなものです(上記はアナロジーであってSubstrateのネットワークでTCP/UDPを使用しないという意味ではない)。ブロックチェーンにおける下部レイヤーをアプリケーションに応じて提供することで、柔軟なDAppsの開発が可能になります。

仮にスクラッチから新たなブロックチェーンを開発しようとすると様々な項目の検討が必要となります。[2]

  • コンセンサスアルゴリズムの検討、耐障害性含むトリレンマ
  • ブロック構造や効率的なストレージのデザイン、メッセージシリアライゼーション
  • P2Pネットワーク(ノードのピアリング、ブロック生成とトランザクション処理)
  • ステートマシーン、ランタイム実行、スマートコントラクトの仕様

Substrateでは様々なコンポーネントをモジュラー化して提供しているため、開発するアプリケーションに応じて必要な機能を加えたり、自由にカスタマイズしてブロックチェーンを展開することが可能です。

TCRって何でしたっけ?って方多いと思います(-_-;)

ここからはTCR(Token Curated Registry)の説明と実装に分けて進めていきます。

  • TCR(Token Curated Registry)とは
  • TCR Party
  • TCRの実装
  • UIでの動作確認(Polkadot Apps UI)の方法

TCR(Token Curated Registry)とは

TCR(Token Curated Registry)は分散型で運用されるリストです。トークン保有者がリスティングの既存参加者や新規の参加申請に対してチャレンジ(異議)を行うことで、参加者のリストへの登録可否を判断します。この分散型の運用によってリストの信頼性や価値を高めることが可能です。信頼性が高まることによってトークンの価値が高まるため、トークン保有者にはこのリストを信頼足りえるように維持する経済的なインセンティブが働く、ということが原理として考えられています。[3]

身近な例で説明してみます。

例えば食べログは飲食店の情報を掲載しているサイトですが、食べログという会社にお店の掲載許可の権限があり(中央集権的)、ユーザにとって有益でない飲食店も載っている可能性があります(食べログはリッチで高機能なサイトですが、仮に食べログを単純な飲食店のリストであると考えてください)。ここに分散型で信頼性のある飲食店のリストを運用する動機が発生してきます。[4]

まずユーザは信頼性のある飲食店のリストを望んでいるものとします。飲食店はこのリストへ掲載されることで売上げが上がるなどのメリットがあるため、このリストへの掲載を希望しています。掲載申請するにはこの分散型リストのトークンをデポジットする必要があります。ただしトークン保有者によってこの申請に対してチャレンジ(異議)が呼び出されると、このデポジットされたトークンはチャレンジに参加したトークン保有者間で没収され分配されます。つまり基本的に申請をする参加者は、チャレンジされると分かり切ってきる申請はおそらく行いません。とあるリストにふさわしくないと思う参加者はそのリストへは申請をしてこないということが考えられます。

またトークン保有者はチャレンジ(異議)を行うことでトークン保有者間でトークンを総取りできますが、そういった行為を繰り返すとこのリストに対する信頼性が著しく低下するためトークン自体の価値が毀損されていきます。ここでトークン保有者にはリストを正しい方向へ導きユーザのニーズを満たす責任があると同時に、この仕組みにはトークンの価値を高める経済的なインセンティブも内在していることが考えられます。

TCR Party

TCR Partyは、Alpine IntelのTCRの実験的な実装で、MetaMaskやトークン購入をせずとも、TCRの仕組みを試すことができるアプリケーションです。Twitter APIを使用することでユーザや参加者はTwitter Botを経由してTCRを使用することができます。ここでリストされているユーザのツイートは全てTwitter Botによってリツイートされるようになっています。[5]

Enter TCR Party, a curated list of the top Twitter handles in the crypto community, powered by an existing human-readable interface.

TCR Partyでは2つのTwitter Botが動作しています。

@TCRPartyBot

TCR Partyの参加者のツイートをリツイートします。

@TCRParyVIP

Twitterのチャットボットで参加・チャレンジ(異議)・投票などをスマートコントラクト側へと橋渡しします。また参加者側への通知なども行います。

使用されているトークン『TCRP』はFaucetから日々供給されますが、トークンやスマートコントラクトはRopstenのテストネットワーク上にデプロイされているので現実的な金銭価値は現在持っていません。

またTCR Partyについては、人気コンテスト・美人投票のような結果になるのではという疑念も浮上しています。[6]

TCRの実装

RustにおけるTCRの実装サンプルです。ここからはSubstrateが既にインストールされている前提で進めます。

$ substrate --version
substrate 1.0.0-3ec6247-x86_64-linux-gnu

※環境はUbuntu 16.04.1 LTSを使用

substrate-node-new コマンドで、指定したプロジェクト名のテンプレートノードおよびプロジェクトのディレクトリを作成します。ここではauthor はtestとしました。このコマンドはテンプレートノードの作成とプロジェクトに必要なファイル群をScaffoldしてくれます。コマンドの本体は Cargo new だと思われます。

$ substrate-node-new <project name> <author> // Example
$ substrate-node-new substrate-node-template test

必要なツール群をインストールします。

$ ./init.sh
*** Initializing WASM build environment
info: syncing channel updates for 'nightly-x86_64-unknown-linux-gnu'
info: checking for self-updates
info: syncing channel updates for 'stable-x86_64-unknown-linux-gnu'
stable-x86_64-unknown-linux-gnu unchanged - rustc 1.34.1 (fc50f328b 2019-04-24)/.cargo/bin/wasm-gc

Wasmランタイムのコンパイルおよびビルドをして、development環境でSubstrate Nodeが動作することを確認しましょう。

※念のためCargoでビルドしなくても ./target/release/template-node --dev でリリースイメージを実行できます

$ ./build.sh
Building webassembly binary in runtime/wasm...
Finished release [optimized] target(s) in 4.20s
$ cargo build
$ cargo run -- --dev
Finished dev [unoptimized + debuginfo] target(s) in 0.69s
Running `target/debug/template-node --dev`
2019-04-28 14:31:05 Substrate Node
2019-04-28 14:31:05 version 1.0.0-d138f61-x86_64-linux-gnu
2019-04-28 14:31:05 by test, 2017, 2018
2019-04-28 14:31:05 Chain specification: Development
2019-04-28 14:31:05 Node name: befitting-songs-1554
2019-04-28 14:31:05 Roles: AUTHORITY
2019-04-28 14:31:06 Best block: #540
2019-04-28 14:31:06 Local node address is: /ip4/0.0.0.0/tcp/30333/p2p/Qmdr9ZLtT37Jp1sXDzy8WGxMRg4K84V5GCDPfSy299rpuc
  • トークンの実装

substrate-tcrのサンプルコードからtoken.rs をコピーしてruntime/src/token.rs として保存します。このコードはERC20のようなインターフェースを持つトークンを提供します。ただしlockunlock という参加者がトークンをデポジットできるファンクションを追加で定義しています。Rustが読めないとつらいですが、SolidityでERC20をコーディングしたことがあれば、同様のインターフェースを実装していることはすぐに理解できると思います。

token.rs

ここからは token.rs モジュールを使用して tcr.rs をコーディングします。2つのモジュールを lib.rs で読み込みビルドする所まで行います。

  • ストレージの実装

Substrate Runtime Module Library(SRML)の timestamp モジュールおよび上記の token.rs クレートをローカルから読み込みます。

use {system::ensure_signed, timestamp};
use crate::token;

トレイト宣言部分です。

pub trait Trait: timestamp::Trait + token::Trait {
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}

TCRではリスティング、チャレンジ(異議)、各トークン保有者の投票( Vote)およびその投票の結果( Poll)を保存する必要があります。それぞれ構造体で定義していきます。

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Listing<U, V, W> {
id: u32,
data: Vec<u8>,
deposit: U,
owner: V,
application_expiry: W,
whitelisted: bool,
challenge_id: u32,
}

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Challenge<T, U, V, W> {
listing_hash: T,
deposit: U,
owner: V,
voting_ends: W,
resolved: bool,
reward_pool: U,
total_tokens: U
}

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Vote<U> {
value: bool,
deposit: U,
claimed: bool,
}

#[derive(Encode, Decode, Default, Clone, PartialEq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct Poll<T, U> {
listing_hash: T,
votes_for: U,
votes_against: U,
passed: bool,
}

ストレージ宣言部を実装する前に、TCR実装で必要なパラメータを確認しておきます。TCRでは参加申請する際に、これらのパラメータを『スナップショット』として保存し、後からでも参照できるようにします。

  • MIN_DEPOSIT — 参加者がデポジットとしてステークするトークン量、このデポジットは参加者がリスティングを終える際には返却可能
  • APPLY_STAGE_LEN — チャレンジ(異議)期間、ブロックかepoch時間で表現され、この期間が過ぎると参加者はリストに加えられる
  • COMMIT_PERIOD_LEN — トークン保有者が特定のチャレンジ(異議)期間の投票に対してコミットできる期間、ブロックかepoch時間で計られる
  • REVEAL_PERIOD_LEN — トークン保有者が特定のチャレンジ(異議)に対してコミットされた投票を開示できる期間、ブロックかepoch時間で計られる
  • DISPENSATION_PCT — チャレンジ(異議)においてチャレンジの多数派のパーティーへ支払われるキャピタルリスクを補うための特別なトークン比率、残りのデポジットされたトークンは多数派の投票者間で分配される
  • VOTE_QUORUM — チャレンジ(異議)において参加者をリスティングするために必要なトークンのパーセンテージ(全トークンでなく対象のチャレンジ内でのトークン比率)、50%であれば50%以上のトークン数を獲得する必要がある(得票数ではない点に注意)

このサンプル実装ではMIN_DEPOSIT、APPLY_STAGE_LEN、COMMIT_PERIOD_LENがgenesis configで定義されています。その他のパラメータは使用されていないようです。

decl_storage へストレージの宣言をします。 Momemtタイプは timestamp モジュールから TokenBalance タイプは先に実装した token モジュールから読み込んでいます。

decl_storage! {
  trait Store for Module<T: Trait> as Tcr {
// stores the owner in the genesis config
Owner get(owner) config(): T::AccountId;
// stores a list of admins who can set config
Admins get(admins): map T::AccountId => bool;
// TCR parameter - minimum deposit
MinDeposit get(min_deposit) config(): Option<T::TokenBalance>;
    // TCR parameter - apply stage length - deadline for challenging before a listing gets accepted
    ApplyStageLen get(apply_stage_len) config(): Option<T::Moment>;

    // TCR parameter - commit stage length - deadline for voting before a challenge gets resolved
    CommitStageLen get(commit_stage_len) config(): Option<T::Moment>;

    // the TCR - list of proposals
    Listings get(listings): map T::Hash => Listing<T::TokenBalance, T::AccountId, T::Moment>;

    // to make querying of listings easier, maintaining a list of indexes and corresponding listing hashes
    ListingCount get(listing_count): u32;
    ListingIndexHash get(index_hash): map u32 => T::Hash;

    // global nonce for poll count
    PollNonce get(poll_nonce) config(): u32;

    // challenges
    Challenges get(challenges): map u32 => Challenge<T::Hash, T::TokenBalance, T::AccountId, T::Moment>;

    // polls
    Polls get(polls): map u32 => Poll<T::Hash, T::TokenBalance>;

    // votes
    // mapping is between a tuple of (poll id, Account Id) and a vec of votes
    // poll and vote have a 1:n relationship
    Votes get(votes): map (u32, T::AccountId) => Vote<T::TokenBalance>;

  }
}

config() を使用することでgenesis configからパラメータ値を取得し設定することができます。 src/chain_spec.rs を編集します。TcrConfigを以下のように追加。

use node_template_runtime::{
AccountId, GenesisConfig, ConsensusConfig, TimestampConfig, BalancesConfig,
SudoConfig, IndicesConfig, TcrConfig
};

testnet_genesis ファンクションの中にパラメータの値をそれぞれ記載します。config() の箇所はこの設定値から代入されます。

tcr: Some(TcrConfig {
// owner account id
owner: ed25519::Pair::from_seed(b"Alice ").public().0.into(),

// min deposit for proposals
min_deposit: 100,

// challenge time limit - for testing its set to 2 mins (120 sec)
apply_stage_len: 120,

// voting time limit - for testing its set to 4 mins (240 sec)
commit_stage_len: 240,

// initial poll/challenge set to 1
// to avoid 0 values
poll_nonce: 1,
})

ストレージの実装は完了です。

  • イベントの実装

クライアントへの通知のためのイベントを実装します。フロントエンドなどと連携してユーザ側へイベント通知できるようになります。Substrateのランタイムのファンクションの返り値は、空かエラーメッセージのみです。サクセスや正は返しません。

イベントのデータをオンチェーンデータのオフチェーンキャッシュとして使用することもできます。

このTCRランタイムでは、参加申請、チャレンジ(異議)、投票、解決(Resolution)、申請許可、申請拒否、報酬(Reward)のイベントを定義していきます。

decl_event!(
pub enum Event<T> where
AccountId = <T as system::Trait>::AccountId,
Balance = <T as token::Trait>::TokenBalance,
Hash = <T as system::Trait>::Hash,
{
// when a listing is proposed
Proposed(AccountId, Hash, Balance),
// when a listing is challenged
Challenged(AccountId, Hash, u32, Balance),
// when a challenge is voted on
Voted(AccountId, u32, Balance),
// when a challenge is resolved
Resolved(Hash, u32),
// when a listing is accepted in the registry
Accepted(Hash),
// when a listing is rejected from the registry
Rejected(Hash),
// when a vote reward is claimed for a challenge
Claimed(AccountId, u32),
}
);

ここまででストレージの定義、genesis config、イベントが実装できたのでロジックの実装へ移っていきます。

  • ロジックの実装

ロジックは decl_moduleへコーディングします。まずはリスティングへの参加申請のロジック( propose)からです。senderからリスティング名とデポジットを引き受け、デポジットのトークン量が足りているか確認します。問題なければ token モジュールの lock でデポジットし、 Listings のストレージへ対象の申請情報を保存します。

// propose a listing on the registry
// takes the listing name (data) as a byte vector
// takes deposit as stake backing the listing
// checks if the stake is less than minimum deposit needed
fn propose(origin, data: Vec<u8>, #[compact] deposit: T::TokenBalance) -> Result {
let sender = ensure_signed(origin)?;

// to avoid byte arrays with unlimited length
ensure!(data.len() <= 256, "listing data cannot be more than 256 bytes");

let min_deposit = Self::min_deposit().ok_or("Min deposit not set")?;
ensure!(deposit >= min_deposit, "deposit should be more than min_deposit");

// set application expiry for the listing
// using the `Timestamp` SRML module for getting the block timestamp
// generating a future timestamp by adding the apply stage length
let now = <timestamp::Module<T>>::get();
let apply_stage_len = Self::apply_stage_len().ok_or("Apply stage length not set.")?;
let app_exp = now.checked_add(&apply_stage_len).ok_or("Overflow when setting application expiry.")?;

let hashed = <T as system::Trait>::Hashing::hash(&data);

let listing_id = Self::listing_count();

// create a new listing instance
let listing = Listing {
id: listing_id,
data,
deposit,
owner: sender.clone(),
whitelisted: false,
challenge_id: 0,
application_expiry: app_exp,
};

ensure!(!<Listings<T>>::exists(hashed), "Listing already exists");

// deduct the deposit for application
<token::Module<T>>::lock(sender.clone(), deposit, hashed.clone())?;

<ListingCount<T>>::put(listing_id + 1);
<Listings<T>>::insert(hashed, listing);
<ListingIndexHash<T>>::insert(listing_id, hashed);

// let the world know
// raise the event
Self::deposit_event(RawEvent::Proposed(sender, hashed.clone(), deposit));
runtime_io::print("Listing created!");

Ok(())}
}

ロジックの途中でエラーが発生してもブロックチェーンのステートをロールバックすることはできません。上記のファンクションでユーザのトークンを lock した後にエラーでロジックが止まってしまわないか注意する必要があります。

次はチャレンジ(異議)のファンクション( challenge)です。ファンクションが呼び出された場合、対象の申請がリスティングに存在するかおよびチャレンジ期間中であるかを確認します。チャレンジに対するステークがリスティングのデポジット以上であるかを確認して、 Challenges のストレージへ保存します。

チャレンジ(異議)はリスティングの候補者もしくは既にリスティングされている参加者どちらに対しても開始できます。

// challenge a listing
// for simplicity, only three checks are being done
// a. if the listing exists
// c. if the challenger is not the owner of the listing
// b. if enough deposit is sent for challenge
fn challenge(origin, listing_id: u32, #[compact] deposit: T::TokenBalance) -> Result {
let sender = ensure_signed(origin)?;

ensure!(<ListingIndexHash<T>>::exists(listing_id), "Listing not found.");

let listing_hash = Self::index_hash(listing_id);
let listing = Self::listings(listing_hash);

ensure!(listing.challenge_id == 0, "Listing is already challenged.");
ensure!(listing.owner != sender, "You cannot challenge your own listing.");
ensure!(deposit >= listing.deposit, "Not enough deposit to challenge.");

// get current time
let now = <timestamp::Module<T>>::get();

// get commit stage length
let commit_stage_len = Self::commit_stage_len().ok_or("Commit stage length not set.")?;
let voting_exp = now.checked_add(&commit_stage_len).ok_or("Overflow when setting voting expiry.")?;

// check apply stage length not passed
// ensure that now <= listing.application_expiry
ensure!(listing.application_expiry > now, "Apply stage length has passed.");

let challenge = Challenge {
listing_hash,
deposit,
owner: sender.clone(),
voting_ends: voting_exp,
resolved: false,
reward_pool: <T::TokenBalance as As<u64>>::sa(0),
total_tokens: <T::TokenBalance as As<u64>>::sa(0),
};

let poll = Poll {
listing_hash,
votes_for: listing.deposit,
votes_against: deposit,
passed: false,
};

// deduct the deposit for challenge
<token::Module<T>>::lock(sender.clone(), deposit, listing_hash)?;

// global poll nonce
// helps keep the count of challenges and in mapping votes
let poll_nonce = <PollNonce<T>>::get();
// add a new challenge and the corresponding poll in the respective collections
<Challenges<T>>::insert(poll_nonce, challenge);
<Polls<T>>::insert(poll_nonce, poll);

// update listing with challenge id
<Listings<T>>::mutate(listing_hash, |listing| {
listing.challenge_id = poll_nonce;
});

// update the poll nonce
<PollNonce<T>>::put(poll_nonce + 1);

// raise the event
Self::deposit_event(RawEvent::Challenged(sender, listing_hash, poll_nonce, deposit));
runtime_io::print("Challenge created!");

Ok(())
}

投票のファンクション( Vote)も作成します。同様に対象の申請がリスティングに存在するかとチャレンジ期間中であるかを確認します。

poll インスタンスの votes_for もしくは votes_against へデポジットを追加してインスタンスを更新し、 Votes のストレージへ新規の Voteの情報を保存します。最終的にこのデポジットの多いパーティーが多数派としてチャレンジ成否が決定されます。

// registers a vote for a particular challenge
// checks if the listing is challenged and
// if the commit stage length has not passed
// to keep it simple, we just store the choice as a bool - true: aye; false: nay
fn vote(origin, challenge_id: u32, value: bool, #[compact] deposit: T::TokenBalance) -> Result {
let sender = ensure_signed(origin)?;

// check if listing is challenged
ensure!(<Challenges<T>>::exists(challenge_id), "Challenge does not exist.");
let challenge = Self::challenges(challenge_id);
ensure!(challenge.resolved == false, "Challenge is already resolved.");

// check commit stage length not passed
let now = <timestamp::Module<T>>::get();
ensure!(challenge.voting_ends > now, "Commit stage length has passed.");

// deduct the deposit for vote
<token::Module<T>>::lock(sender.clone(), deposit, challenge.listing_hash)?;

let mut poll_instance = Self::polls(challenge_id);
// based on vote value, increase the count of votes (for or against)
match value {
true => poll_instance.votes_for += deposit,
false => poll_instance.votes_against += deposit,
}

// create a new vote instance with the input params
let vote_instance = Vote {
value,
deposit,
claimed: false,
};

// mutate polls collection to update the poll instance
<Polls<T>>::mutate(challenge_id, |poll| *poll = poll_instance);

// insert new vote into votes collection
<Votes<T>>::insert((challenge_id, sender.clone()), vote_instance);

// raise the event
Self::deposit_event(RawEvent::Voted(sender, challenge_id, deposit));
runtime_io::print("Vote created!");
Ok(())
}

解決(Resolution)のロジック実装です。参加申請に対してチャレンジ(異議)のなかったものでAPPLY_STAGE_LENを経過しているもの、もしくはチャレンジのあった申請でCOMMIT_PERIOD_LENを経過済みのリスティングに対してファンクションが呼び出されます。

listing.whitelisted という真偽値でリスティングが申請許可されたか、申請拒否されたかを設定します。既に存在するリスティングに対するチャレンジにおいても同様で、 true の場合に掲載が許可されたことになります。

// resolves the status of a listing
fn resolve(_origin, listing_id: u32) -> Result {
ensure!(<ListingIndexHash<T>>::exists(listing_id), "Listing not found.");

let listing_hash = Self::index_hash(listing_id);
let listing = Self::listings(listing_hash);

let now = <timestamp::Module<T>>::get();
let challenge;
let poll;

// check if listing is challenged
if listing.challenge_id > 0 {
// challenge
challenge = Self::challenges(listing.challenge_id);
poll = Self::polls(listing.challenge_id);

// check commit stage length has passed
ensure!(challenge.voting_ends < now, "Commit stage length has not passed.");
} else {
// no challenge
// check if apply stage length has passed
ensure!(listing.application_expiry < now, "Apply stage length has not passed.");

// update listing status
<Listings<T>>::mutate(listing_hash, |listing|
{
listing.whitelisted = true;
});

Self::deposit_event(RawEvent::Accepted(listing_hash));
return Ok(());
}

let mut whitelisted = false;

// mutate polls collection to update the poll instance
<Polls<T>>::mutate(listing.challenge_id, |poll| {
if poll.votes_for >= poll.votes_against {
poll.passed = true;
whitelisted = true;
} else {
poll.passed = false;
}
});

// update listing status
<Listings<T>>::mutate(listing_hash, |listing| {
listing.whitelisted = whitelisted;
listing.challenge_id = 0;
});

// update challenge
<Challenges<T>>::mutate(listing.challenge_id, |challenge| {
challenge.resolved = true;
if whitelisted == true {
challenge.total_tokens = poll.votes_for;
challenge.reward_pool = challenge.deposit + poll.votes_against;
} else {
challenge.total_tokens = poll.votes_against;
challenge.reward_pool = listing.deposit + poll.votes_for;
}
});

// raise appropriate event as per whitelisting status
if whitelisted == true {
Self::deposit_event(RawEvent::Accepted(listing_hash));
} else {
// if rejected, give challenge deposit back to the challenger
<token::Module<T>>::unlock(challenge.owner, challenge.deposit, listing_hash)?;
Self::deposit_event(RawEvent::Rejected(listing_hash));
}

Self::deposit_event(RawEvent::Resolved(listing_hash, listing.challenge_id));
Ok(())
}

最後に報酬(Reward)のロジック実装です。チャレンジが解決されると多数派のパーティーは報酬(Reward)を受け取ることができます。

claim_reward では対象のチャレンジが解決されているかおよび sender が対象のチャレンジに対して投票していたかを確認します。投票が多数派で報酬(Reward)がある場合は計算を行い unlock でトークンを分配します。最後に Vote インスタンスを処理済みにして同じ投票による報酬(Reward)を2度受けられないようにしておきます。

// claim reward for a vote
fn claim_reward(origin, challenge_id: u32) -> Result {
let sender = ensure_signed(origin)?;

// ensure challenge exists and has been resolved
ensure!(<Challenges<T>>::exists(challenge_id), "Challenge not found.");
let challenge = Self::challenges(challenge_id);
ensure!(challenge.resolved == true, "Challenge is not resolved.");

// get the poll and vote instances
// reward depends on poll passed status and vote value
let poll = Self::polls(challenge_id);
let vote = Self::votes((challenge_id, sender.clone()));

// ensure vote reward is not already claimed
ensure!(vote.claimed == false, "Vote reward has already been claimed.");

// if winning party, calculate reward and transfer
if poll.passed == vote.value {
let reward_ratio = challenge.reward_pool.checked_div(&challenge.total_tokens).ok_or("overflow in calculating reward")?;
let reward = reward_ratio.checked_mul(&vote.deposit).ok_or("overflow in calculating reward")?;
let total = reward.checked_add(&vote.deposit).ok_or("overflow in calculating reward")?;
<token::Module<T>>::unlock(sender.clone(), total, challenge.listing_hash)?;

Self::deposit_event(RawEvent::Claimed(sender.clone(), challenge_id));
}

// update vote reward claimed status
<Votes<T>>::mutate((challenge_id, sender), |vote| vote.claimed = true);

Ok(())
}

基本的なロジックの実装は完了です。

runtime/src/lib.rs で作成したモジュールを読み込みます。以下太字が追加した箇所です。

/// Used for the module template in `./template.rs`
// mod template;
mod token;
mod tcr;
impl tcr::Trait for Runtime {
type Event = Event;
}
impl token::Trait for Runtime {
type Event = Event;
type TokenBalance = u128;
}
construct_runtime!(
pub enum Runtime with Log(InternalLog: DigestItem<Hash, AuthorityId, AuthoritySignature>) where
Block = Block,
NodeBlock = opaque::Block,
UncheckedExtrinsic = UncheckedExtrinsic
{
System: system::{default, Log(ChangesTrieRoot)},
Timestamp: timestamp::{Module, Call, Storage, Config<T>, Inherent},
Consensus: consensus::{Module, Call, Storage, Config<T>, Log(AuthoritiesChange), Inherent},
Aura: aura::{Module},
Indices: indices,
Balances: balances,
Sudo: sudo,
// Used for the module template in `./template.rs`
// TemplateModule: template::{Module, Call, Storage, Event<T>},
Tcr: tcr::{Module, Call, Storage, Event<T>, Config<T>},
Token: token::{Module, Call, Storage, Event<T>, Config<T>},
}
);

Wasmランタイムでのコンパイルおよびネイティブのビルドを行います。

$ ./build.sh
Building webassembly binary in runtime/wasm...
Compiling substrate-node-template-runtime v1.0.0 (/substrate-node-template/runtime)
...
Compiling substrate-node-template-runtime-wasm v1.0.0 (/substrate-node-template/runtime/wasm)
Finished release [optimized] target(s) in 39.45s
$ cargo build --release

UIでの動作確認の方法

アプリケーションの動作確認はこちらのサイトを参考にしてみてください。Vol.1と同様にPolkadot Apps UIにローカルノードを接続して確認ができます。[7]

以上です。

まとめ

  • Substrateはプログラミング言語で記述可能なブロックチェーンのライブラリ群である
  • TCRは分散型のリストでトークン保有者により非中央集権的に運用される
  • TCR PartyはTwitterアカウントとTwitter Botで運用されるTCRの実装で、Ethereumのテストネットワーク上で動いている
  • Substrate TCRのサンプルを実装してビルドできた

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet