Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
6
Help us understand the problem. What is going on with this article?
@yama_mo

完全にrandomな数字を生み出すための smart contract RANDAO

More than 3 years have passed since last update.

EthereumでDappを作ろうと思ったときに手数料やトランザクションの次くらいに困るのがrandamな番号の生成かと思います。
blockhashやtimestampなどの値は、ある程度マイナーに依存した数値になってしまうため、そのまま利用するとマイナーに依存する不完全なものになってしまいます。

randaoは、contractに参加する複数の人間から得られた入力値を乱数生成に利用し、さらに経済的なインセンティブを与えることによってランダム性を担保しよう、という試みです。

こちらに書いてあるので、普通に英語でこちら見ても良いと思います。

以下解説

STEP 1

乱数生成に参加したい参加者は、誓約(pledge)として、決められた期間の間に、あらかじめ決められた量のETHを、それぞれが選択した秘密の番号sのhash sha3(s) と一緒にrandao contractに送金します。

STEP 2

参加者は、step1で送った番号sをコントラクトに送り、コントラクトはそれをsha3(s)して、step1で受け取ったhashの情報と一致することを確認し、一致した場合は有効なseedとしてsをseedの中に追加し、最終的にseedを元にrandam numberを生成する。

STEP 3

全てのs(step1で集めた秘密の数字)を集めることに成功したら、random numberを関数f(s1,s2,s2...)によってい生成し、乱数を要求していたコントラクトに対して結果を送信します。

さらにrandaoコントラクトは、pledgeとして受け取っていたETHと、乱数を要求するための手数料として他のコントラクトがrandaoコントラクトに支払っていたETHを合わせたものを、報酬として乱数生成に協力した人たちに送金します。

以上がRANDAOの概要です。このようにして、乱数生成に対して一定のfeeを要求し、生成に協力した人間にそのfeeを支払う、という形を取っています。

以下は実際のコントラクトの解説をします。

Counter

 contract Counter { 
   uint public counter = 0;
   function count() { 
     counter += 1;    
   } 
 } 

文字通り、数えて行くだけのコントラクト。count()が呼ばれたらstate変数のcounterに+1して行く。

Randao

contract Randao {
  struct Participant {
    uint256   secret;
    bytes32   commitment;
    uint256   reward;
    bool      revealed;
    bool      rewarded;
  }

  struct Consumer {
    address caddr;
    uint256 bountypot;
  }

  struct Campaign {
    uint32    bnum;
    uint96    deposit;
    uint16    commitBalkline;
    uint16    commitDeadline;

    uint256   random;
    bool      settled;
    uint256   bountypot;
    uint32    commitNum;
    uint32    revealsNum;

    mapping (address => Consumer) consumers;
    mapping (address => Participant) participants;
  }

  uint256 public numCampaigns;
  Campaign[] public campaigns;
  address public founder;

  modifier blankAddress(address _n) { if (_n != 0) throw; _; }

  modifier moreThanZero(uint256 _deposit) { if (_deposit <= 0) throw; _; }

  modifier notBeBlank(bytes32 _s) { if (_s == "") throw; _; }

  modifier beBlank(bytes32 _s) { if (_s != "") throw; _; }

  modifier beFalse(bool _t) { if (_t) throw; _; }

  function Randao() {
    founder = msg.sender;
  }

  event LogCampaignAdded(uint256 indexed campaignID,
    address indexed from,
    uint32 indexed bnum,
    uint96 deposit,
    uint16 commitBalkline,
    uint16 commitDeadline,
    uint256 bountypot);

  modifier timeLineCheck(uint32 _bnum, uint16 _commitBalkline, uint16 _commitDeadline) {
    if (block.number >= _bnum) throw;
    if (_commitBalkline <= 0) throw;
    if (_commitDeadline <= 0) throw;
    if (_commitDeadline >= _commitBalkline) throw;
    if (block.number >= _bnum - _commitBalkline) throw;
    _;
  }

  function newCampaign(
    uint32 _bnum,
    uint96 _deposit,
    uint16 _commitBalkline,
    uint16 _commitDeadline
  ) payable
  timeLineCheck(_bnum, _commitBalkline, _commitDeadline)
  moreThanZero(_deposit) external returns (uint256 _campaignID) {
    _campaignID = campaigns.length++;
    Campaign c = campaigns[_campaignID];
    numCampaigns++;
    c.bnum = _bnum;
    c.deposit = _deposit;
    c.commitBalkline = _commitBalkline;
    c.commitDeadline = _commitDeadline;
    c.bountypot = msg.value;
    c.consumers[msg.sender] = Consumer(msg.sender, msg.value);
    LogCampaignAdded(_campaignID, msg.sender, _bnum, _deposit, _commitBalkline, _commitDeadline, msg.value);
  }

  event LogFollow(uint256 indexed CampaignId, address indexed from, uint256 bountypot);

  function follow(uint256 _campaignID)
  external payable returns (bool) {
    Campaign c = campaigns[_campaignID];
    Consumer consumer = c.consumers[msg.sender];
    return followCampaign(_campaignID, c, consumer);
  }

  modifier checkFollowPhase(uint256 _bnum, uint16 _commitDeadline) {
    if (block.number > _bnum - _commitDeadline) throw;
    _;
  }

  function followCampaign(
    uint256 _campaignID,
    Campaign storage c,
    Consumer storage consumer
  ) checkFollowPhase(c.bnum, c.commitDeadline)
  blankAddress(consumer.caddr) internal returns (bool) {
    c.bountypot += msg.value;
    c.consumers[msg.sender] = Consumer(msg.sender, msg.value);
    LogFollow(_campaignID, msg.sender, msg.value);
    return true;
  }

  event LogCommit(uint256 indexed CampaignId, address indexed from, bytes32 commitment);

  function commit(uint256 _campaignID, bytes32 _hs) notBeBlank(_hs) external payable {
    Campaign c = campaigns[_campaignID];
    commitmentCampaign(_campaignID, _hs, c);
  }

  modifier checkDeposit(uint256 _deposit) { if (msg.value != _deposit) throw; _; }

  modifier checkCommitPhase(uint256 _bnum, uint16 _commitBalkline, uint16 _commitDeadline) {
    if (block.number < _bnum - _commitBalkline) throw;
    if (block.number > _bnum - _commitDeadline) throw;
    _;
  }

  function commitmentCampaign(
    uint256 _campaignID,
    bytes32 _hs,
    Campaign storage c
  ) checkDeposit(c.deposit)
  checkCommitPhase(c.bnum, c.commitBalkline, c.commitDeadline)
  beBlank(c.participants[msg.sender].commitment) internal {
    c.participants[msg.sender] = Participant(0, _hs, 0, false, false);
    c.commitNum++;
    LogCommit(_campaignID, msg.sender, _hs);
  }

  // For test
  function getCommitment(uint256 _campaignID) external constant returns (bytes32) {
    Campaign c = campaigns[_campaignID];
    Participant p = c.participants[msg.sender];
    return p.commitment;
  }

  function shaCommit(uint256 _s) returns (bytes32) {
    return sha3(_s);
  }

  event LogReveal(uint256 indexed CampaignId, address indexed from, uint256 secret);

  function reveal(uint256 _campaignID, uint256 _s) external {
    Campaign c = campaigns[_campaignID];
    Participant p = c.participants[msg.sender];
    revealCampaign(_campaignID, _s, c, p);
  }

  modifier checkRevealPhase(uint256 _bnum, uint16 _commitDeadline) {
    if (block.number <= _bnum - _commitDeadline) throw;
    if (block.number >= _bnum) throw;
    _;
  }

  modifier checkSecret(uint256 _s, bytes32 _commitment) {
    if (sha3(_s) != _commitment) throw;
    _;
  }

  function revealCampaign(
    uint256 _campaignID,
    uint256 _s,
    Campaign storage c,
    Participant storage p
  ) checkRevealPhase(c.bnum, c.commitDeadline)
  checkSecret(_s, p.commitment)
  beFalse(p.revealed) internal {
    p.secret = _s;
    p.revealed = true;
    c.revealsNum++;
    c.random ^= p.secret;
    LogReveal(_campaignID, msg.sender, _s);
  }

  modifier bountyPhase(uint256 _bnum){ if (block.number < _bnum) throw; _; }

  function getRandom(uint256 _campaignID) external returns (uint256) {
    Campaign c = campaigns[_campaignID];
    return returnRandom(c);
  }

  function returnRandom(Campaign storage c) bountyPhase(c.bnum) internal returns (uint256) {
    if (c.revealsNum == c.commitNum) {
      c.settled = true;
      return c.random;
    }
  }

  // The commiter get his bounty and deposit, there are three situations
  // 1. Campaign succeeds.Every revealer gets his deposit and the bounty.
  // 2. Someone revels, but some does not,Campaign fails.
  // The revealer can get the deposit and the fines are distributed.
  // 3. Nobody reveals, Campaign fails.Every commiter can get his deposit.
  function getMyBounty(uint256 _campaignID) external {
    Campaign c = campaigns[_campaignID];
    Participant p = c.participants[msg.sender];
    transferBounty(c, p);
  }

  function transferBounty(
    Campaign storage c,
    Participant storage p
  ) bountyPhase(c.bnum)
  beFalse(p.rewarded) internal {
    if (c.revealsNum > 0) {
      if (p.revealed) {
        uint256 share = calculateShare(c);
        returnReward(share, c, p);
      }
      // Nobody reveals
    } else {
      returnReward(0, c, p);
    }
  }

  function calculateShare(Campaign c) internal returns (uint256 _share) {
    // Someone does not reveal. Campaign fails.
    if (c.commitNum > c.revealsNum) {
      _share = fines(c) / c.revealsNum;
      // Campaign succeeds.
    } else {
      _share = c.bountypot / c.revealsNum;
    }
  }

  function returnReward(
    uint256 _share,
    Campaign storage c,
    Participant storage p
  ) internal {
    p.reward = _share;
    p.rewarded = true;
    if (!msg.sender.send(_share + c.deposit)) {
      p.reward = 0;
      p.rewarded = false;
    }
  }

  function fines(Campaign c) internal returns (uint256) {
    return (c.commitNum - c.revealsNum) * c.deposit;
  }

  // If the campaign fails, the consumers can get back the bounty.
  function refundBounty(uint256 _campaignID) external {
    Campaign c = campaigns[_campaignID];
    returnBounty(_campaignID, c);
  }

  modifier campaignFailed(uint32 _commitNum, uint32 _revealsNum) {
    if (_commitNum == _revealsNum && _commitNum != 0) throw;
    _;
  }

  modifier beConsumer(address _caddr) {
    if (_caddr != msg.sender) throw;
    _;
  }

  function returnBounty(uint256 _campaignID, Campaign storage c)
  bountyPhase(c.bnum)
  campaignFailed(c.commitNum, c.revealsNum)
  beConsumer(c.consumers[msg.sender].caddr) internal {
    uint256 bountypot = c.consumers[msg.sender].bountypot;
    c.consumers[msg.sender].bountypot = 0;
    if (!msg.sender.send(bountypot)) {
      c.consumers[msg.sender].bountypot = bountypot;
    }
  }
}

結構長いコントラクトでした。順番に

変数

struct Participant 乱数生成に参加する人
secret → 秘密の数字
commitment → 秘密の数字のhash sha3(secret)
reward → 報酬
revealed → 有効と証明されたか
rewarded → 報酬を受け取ったか

Consumer 乱数を要求するコントラクト
caddr → 乱数を要求するコントラクトのアドレス
bountypot → comsumerが支払った手数料

Campaign
bnum → randam numberを生成するblock number
deposit → 参加者に求めるdeposit
commitBalkline → follow phaseの締め切り。(randam numberをrequestする締め切り)
commitDeadline → 生成に参加する締め切り。commit phaseの締め切り
random → randam numberを格納
settled → 生成が終わったかどうか
bountypot → 集めた手数料の合計
commitNum → 乱数生成に参加する人の数
revealsNum → hashを証明した人の数
comsumers → 乱数を要求したコントラクトの数
participants →乱数生成参加者の情報

numCampaigns

campaigns

founder

修飾子(modifier)

blankAddress

moreThanZero

notBeBlank

beBlank

beFalse

timeLineCheck

checkFollowPhase

checkRevealPhase

bountyPhase

campaignFailed

beConsumer

phaseは
follow phase → commit phase → reveal phase → bounty phase と移行して行く。

checkSecret
revealで渡された数字とhashが正しいかどうか確認する

Event

LogCampaignAdded

LogFollow

関数(functions)

関数だけサクッと説明します

Randao constructor
コンストラクタ。コントラクトをdeployした人間を決定する。

newCampaign(external)
新しい乱数を生成するキャンペーンを生成する。
関数を実行した人はそのcampaignのconsumerに追加される

commit(external)
hashと参加するcampaignを選択して、campaignに参加する

follow (external)
乱数を要求する。(consumerとして登録する)

commitmentCampaign
新しい参加者をcommitmentに追加する

getCommitment (external)
参加者のhashを取得する

getRandom (external)
campaignによって生成されたrandam numberを返す。

shaCommit
数字を受けて、sha3 hashを返す

reveal (external)
参加者が送信した値とcampaiginのidを指定して、自分がcommitしていることを証明する

revealCampaign
campaign参加者の状態変数を更新しておく

returnRandom
commitした人が全員証明されていることを確認して、random変数を返す

getMyBounty (external)
自分の報酬をgetする

transferBounty
乱数生成の報酬をを参加者に送金する

fines
誰かがrevealに失敗していた場合に、懲罰金として他のcommiterに配分される金額を計算する。→revealに失敗した人の分のdepositが他の人に配分される。

calculateShare
campaignが終わった時に、commitした人間に配分される量を計算する。

returnReward
報酬を配分する

refundBounty (external)
campaignが失敗した時に、comsumerに手数料を返金する

returnBounty
campaignが失敗した時に、comsumerに手数料を返金する

色々問題はありそうですが、大規模に人数集めてやったら上手いこと実現できそうな気はします。。
Ethereumでの乱数生成、悩ましいですね。

6
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
yama_mo
BlockchainはEthereumのsmart contract、Cosmos SDK, WebフロントエンドはReactJS/Typescript、バックエンドはRuby on Railsやgolangなど触っています。Dockerも少しだけ。最近は心理学もやってます。個人開発やスタートアップばかりだったので広く浅くって感じです。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
6
Help us understand the problem. What is going on with this article?