背景
- 研究室の全員が同じ場所に居ないときに、人数制限のあるイベントに誰が行けるかを決めたい。
- 既存のルーレットアプリをpc画面を録画しながら実行したものをslackに流して決めたとしても、自分が望む結果が出るまで撮り直しをすれば不正ができてしまう。
- ブロックチェーンに結果を記録すれば、撮り直しなどの不正を防ぐことができるのでは?
のような動機で作成しました。
手法の結論から言うと、1アドレスにつき1回の乱数生成に制限することでやり直しによる不正を防いでいます。コントラクトは、ropstenにデプロイして、基本的に全ての出力がコンソールに出ます。フロントエンドnoobなので、非常にひどいUIになっています涙。Herokuの関係で停止している事がありますがURL:(https://roulethereum.herokuapp.com) また、Metamaskのインストールが必要です。
使用したもの
研究で用いているのがEthereumなので記録するチェーンはEthereumを使用して、ローカルのテストではGanacheを使用し、関数のテストなどはremixでテストしました。感想としてはやっぱりremixの関数のテストが便利すぎですね。テストネットへのデプロイはEthereumのホスティングサービスであるinfuraを使用しました。
フロントエンドについてはtruffleの公式がReactを推しているためReactを選択し、今回は作業の簡略化のためNext.jsを用いました。
コントラクト
onlyOwnerなどで、デプロイしたアドレス以外の実行を弾いたりしたかったのですが使用しやすいようにonlyOwnerは結局使っていません...
pragma solidity ^0.4.19;
import "./Owned.sol";
contract Roulette is Owned{
// owner can deploy contract at once;
uint public deployTime = 0;
constructor(){
ownerAddr = msg.sender;
deployTime += 1;
}
modifier onlyOnce{
require(deployTime == 0);
_;
}
// candidates
string[] public userNames;
// roulette winner
uint public winner;
// userAddress => make Random Number Times
// user can make random number only once
mapping(address => uint ) public makeRandomNumberTimes;
// set candidate
function setUserName(string _userNames) public {
userNames.push(_userNames);
}
// NOTE : this function uses blockhash and it is NOT secure !
function generateRandomNumber() public{
require(makeRandomNumberTimes[msg.sender] == 0,"you already make random number");
makeRandomNumberTimes[msg.sender]++;
uint userNumber = userNames.length;
bytes32 blockhash = block.blockhash(block.number - 1);
uint mywinner = uint32(blockhash) % (userNumber+1);
winner = mywinner ;
}
// return wineer name
function viewResult() public view returns(string){
return userNames[winner];
}
function viewUsers() public view returns(uint){
return userNames.length;
}
}
詰まりどころ
乱数生成
勝者の決定には、乱数の生成が不可欠です。
Ethereum上での乱数生成は、完全にセキュアなものは存在せず自分が調べた限りですが、この方法が今の所は最適だと思います。しかし、手順が多いので今回はブロックハッシュを用いることにしました。ブロックハッシュを用いる手法も安全ではないのですが、今回は研究室内という小さなコミュニティなのでよしとします。
// create random number (0 ~ (userNumber-1) )
function getBlockHash() public{
uint userNumber = 4;
bytes32 myblockhash = blockhash(block.number - 1);
uint mywinner = uint(myblockhash) % (userNumber);
winner = mywinner;
}
フロントとの連携
最も自分が詰まったのはフロントとの連携です。結論から言うと、コントラクトのインスタンスを作成して、インスタンスの関数をhandleChangeやonClickにかませてやればいいだけです。
// client/web3/provider.js
import Web3 from "web3"
import contract from "truffle-contract"
const provider = () => {
// If the user has MetaMask:
if (typeof web3 !== 'undefined') {
return web3.currentProvider
} else {
console.error("You need to install MetaMask for this app to work!")
}
}
export const getInstance = artifact => {
const contractObj = contract(artifact)
contractObj.setProvider(provider())
return contractObj.deployed()
}
export const eth = new Web3(provider()).eth
getInstance
でインスタンスを作成し、メソッドを定義していく.
どのアカウントで実行するかを明示的に指定しないといけない点に注意すること。
つまり、以下のようにaccountを取得する必要がある。
export const somefunction = async() => {
const storage = await getInstance(Roulette)
const addresses = await eth.getAccounts() // accountを取得
await storage.somefunction({from:addresses[0]}) //accountを指定
}
インスタンスを用いてメソッドを定義
import { eth, getInstance } from './provider'
import Roulette from "../web3/artifacts/Roulette.json"
import Web3 from "web3"
const {
utils: {
hexToString,
},
} = Web3
export const getOwnerInfo = async() => {
const storage = await getInstance(Roulette)
const ownerProfile = await storage.ownerAddr.call()
return ownerProfile
}
export const getDeployrInfo = async() => {
const storage = await getInstance(Roulette)
const deployTime = await storage.deployTime.call()
const result = deployTime.toNumber()
return result
}
export const setName = async(name) =>{
const storage = await getInstance(Roulette)
const addresses = await eth.getAccounts()
await storage.setUserName(name,{from:addresses[0]})
}
export class SetUserInfo extends React.Component{
constructor(props){
super(props);
this.state = {value:''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
async handleChange(event){
this.setState({value:event.target.value});
}
async handleSubmit(event){
console.log("User was Created:" + this.state.value);
event.preventDefault();
await setName(this.state.value)
}
render(){
return(
<form onSubmit={this.handleSubmit}>
<label>
Input user Name:
<input type="text" value={this.state.value} onChange={this.handleChange}/>
</label>
<input type="submit" value="Submit"/>
</form>
)
}
}
export const generateRandom = async() => {
const storage = await getInstance(Roulette)
const addresses = await eth.getAccounts()
await storage.generateRandomNumber({from:addresses[0]})
}
export const viewResult = async() => {
const storage = await getInstance(Roulette)
const addresses = await eth.getAccounts()
const result = await storage.viewResult({from:addresses[0]})
return result
}
export const viewUsers = async() => {
const storage = await getInstance(Roulette)
const addresses = await eth.getAccounts()
const result = await storage.viewUsers({from:addresses[0]})
return result
}
index.jsで呼ぶ
import {eth,getInstance} from "../web3/provider"
import Roulette from "../web3/artifacts/Roulette"
import {getOwnerInfo, getDeployrInfo, SetUserInfo, viewResult, generateRandom, viewUsers} from "../web3/roulette"
export default class IndexPage extends React.Component{
owner = async () => {
const ownerInfo = await getOwnerInfo()
console.log(ownerInfo)
}
deploy = async() => {
const deployInfo = await getDeployrInfo()
console.log(deployInfo)
}
random = async() => {
await generateRandom()
const storage = await getInstance(Roulette)
const temp = await storage.winner.call()
const winner = await temp.toNumber()
console.log("random number : ",winner)
}
users = async() => {
const users = await viewUsers()
const result = await users.toNumber()
console.log(result)
}
result = async() => {
const winner_result = await viewResult()
console.log("winner name : ",winner_result)
}
async componentDidMount() {
const addresses = await eth.getAccounts()
console.log("Your address : ",addresses)
}
render() {
return (
<div>
<h1>Roulette on Ethereum</h1>
<h3>Please push buttons from the top</h3>
<ul>
<li>
<button onClick={this.owner}>
Get Owner address
</button>
</li>
<li>
<button onClick={this.deploy}>
Get deploy time
</button>
</li>
<li>
<SetUserInfo />
</li>
<li>
<button onClick={this.users}>
Get Users Number
</button>
</li>
<li>
<button onClick={this.random}>
Make random number
</button>
</li>
<li>
<button onClick={this.result}>
Show result
</button>
</li>
</ul>
</div>
)
}
}
こんな感じになる
全コードはこちらにあります。
まとめ
今回はブロックハッシュを乱数生成に用いましたが、堅牢かつ手順の少ないもっと良い方法があればなあ...という感じです。あとフロント頑張らないと...