Ethereum スマートコントラクト開発日記(3)
前回は Browser-Solidity 上で単純なコントラクトを動かしました。
Browser-Solidity を開いた多くの人は最初にサンプルソースとして入っている ballot.sol を閉じるか無視するかしていると思います。せっかくだから今回は ballot.sol の動きを追っていきたいと思います。
Ballot 概要
Ballot は単純な投票のシステムを実現するスマートコントラクトです。
コントラクトを作成したアカウント(主催者)が
- 提案の作成
- 投票権の付与
を行い、投票権を各アカウントに付与する事ができます。
投票権を得たアカウント(投票者)は
- 投票の実行
- 投票の委譲(〇〇さんと同じものに投票)
を行う事ができます。
そして、投票後には
- 投票結果(一番投票数の多い提案)の取得
ができます。以上の機能を実現する為にどう書かれているのか見ていきます。
データ構造
// 投票者
struct Voter {
uint weight; // 投票の重み(1固定)
bool voted; // 投票済みフラグ
uint8 vote; // 投票先
address delegate; // 委譲先
}
// 提案
struct Proposal {
uint voteCount; // 投票数
}
address chairperson; // 主催者のアドレス
mapping(address => Voter) voters; // 投票者
Proposal[] proposals; // 提案
コメントつけました。コントラクト内に持つものは主催者のアドレス、各投票者ごとに投票状態・投票先等のステータスを持つマップ、提案の配列にそれぞれの投票数を持ちます。
各機能の実装
次に各メソッドを動かしつつ動作を見ていきます。
提案の作成
function Ballot(uint8 _numProposals) public {
chairperson = msg.sender;
voters[chairperson].weight = 1;
proposals.length = _numProposals;
}
Browser-Solidity 上で _numProposals に数を設定し、Create ボタンを押します。
- 実行アカウント(msg.sender) が投票の主催者として設定されます。
- 主催者も投票者の一人として voters に加え、weight を1与えます。
- _numProposals 個の提案が確保されます。
投票権の付与
function giveRightToVote(address toVoter) public {
if (msg.sender != chairperson || voters[toVoter].voted) return;
voters[toVoter].weight = 1;
}
Browser-Solidity 上で投票権を与えたいアカウントを指定して giveRightToVote を実行します。
指定したアカウントが voters に加え、weight を1与えます。
また、このメソッドを呼び出したアカウントが主催者のものではなかった場合・指定したアカウントが既に投票済であった場合は何もしません。
投票の実行
function vote(uint8 toProposal) public {
Voter storage sender = voters[msg.sender];
if (sender.voted || toProposal >= proposals.length) return;
sender.voted = true;
sender.vote = toProposal;
proposals[toProposal].voteCount += sender.weight;
}
Browser-Solidity 上で投票する提案の番号を指定し、 vote を実行します。
voters からトランザクションを呼び出したアカウントの情報を引き、投票済フラグと投票先を設定します。また、投票先の提案の voteCount を weight 分加算します。
既に投票済み・投票した提案の番号が不正であった場合は何もしません。
storage というのはブロックチェーン内に更新内容を記録する事を示し、storage 指定で取得した sender への変更はブロックチェーン内に永続化されます。
memory を指定すると sender への変更は永続化されず、Gasも消費しません。
構造体への参照を取るかローカルのメモリ上にコピーされてそこをいじるかというイメージで覚えておくと分かりやすいです。
ちなみにこのサンプルソースでは weight は常に1なので1票分ですが、アカウントによって付与する weight の数を変えればこの人の投票はn票分の価値とかできるわけですね。
投票結果の取得
function winningProposal() public constant returns (uint8 _winningProposal) {
uint256 winningVoteCount = 0;
for (uint8 prop = 0; prop < proposals.length; prop++)
if (proposals[prop].voteCount > winningVoteCount) {
winningVoteCount = proposals[prop].voteCount;
_winningProposal = prop;
}
}
これは単純。proposals[] から一番 voteCount が大きいものを探してその index を返してるだけ。
関数定義の constant はコントラクトの状態を変更しないという事を示します。
戻り値の返し方は普通に return文 で明示的に返す以外に戻り値用変数に名前を付けておくと変数がローカルで確保され、関数を抜ける時にその変数の値を返すという返し方ができます。
投票の委譲
function delegate(address to) public {
Voter storage sender = voters[msg.sender]; // assigns reference
if (sender.voted) return;
while (voters[to].delegate != address(0) && voters[to].delegate != msg.sender)
to = voters[to].delegate;
if (to == msg.sender) return;
sender.voted = true;
sender.delegate = to;
Voter storage delegateTo = voters[to];
if (delegateTo.voted)
proposals[delegateTo.vote].voteCount += sender.weight;
else
delegateTo.weight += sender.weight;
}
これはおまけっぽいので最後に。投票の委譲です。主な処理を書き出すと
- 委譲先(to) の投票者情報を取得し、委譲先が既に別の投票者に委譲していたらtoを更新して最終的な委譲先となる投票者(delegateTo)を得る
- メソッドを呼び出した投票者の投票済フラグ・委譲先を設定する
- 委譲先の投票者情報が投票済であった場合、投票した提案の voteCount を weight数加算、まだ投票を行っていなかった場合投票者情報の weight に委譲元の weight を加算しています(委譲先が投票または別のアカウントに委譲を行った場合、累計の weight 数分票が加算される事になります)。
まとめ
ソースの流れは以上です。後は適当に提案を作って適当にアカウント切り替えつつ投票なり委譲なりして動作を確認してください。
単純な作りなので条件によって付与する票数を変えたり他のメソッドを足したりして改造してみるのも面白いと思います。