Y

Reality Keysを使ったWebサービス「BY MY COIN」について(2/4)

f:id:yzono:20150210234955j:plain

はじめに

前回はBY MY COINの使い方について書きました。今回からはどうやって実装しているかについて調べます。

主なユースケースとして以下3つあります。今回は1.契約のセットアップについて書きます。

  1. 契約のセットアップ
  2. 初期表示とセットアップ後の待ち状態での画面表示
  3. Claim実行

目次

知りたいこと

以下のことが分かることをゴールにします。

  • RK APIのリクエスト、レスポンス仕様。サンプルデータ
  • マルチシグアドレス、P2SHアドレスのJSでの作成
  • トランザクションのブロードキャスト方法(特にclaim時)
  • 契約データの保存方法
  • 豚アイコンの意味

RK RunKeeper API

Reality Keysが提供するRunKeeper APIの仕様はこれです。

リクエスト

Send a POST request to: 
https://www.realitykeys.com/api/v1/runkeeper/new

パラメータ

user_id  RunKeeper ID    = 29908850  
activity    アクティビティ running または = walking   
measurement 到達距離タイプ   トータル または 累積   = total_distance
comparison  比較 ge(等しいまたは大きい) lt(小さい)    = ge    
goal    距離(m)   = 4000  
settlement_date 期日 = 2014-09-23 
objection_period_secs   事実認証までの時間 = 604800    
accept_terms_of_service 規約のバージョン = current  use_existing    
use_existing アップデートする場合は1 = 1

リクエスト例

wget -qO- https://www.realitykeys.com/api/v1/runkeeper/new --post-data="user_id=29908850&activity=running&measurement=total_distance&comparison=ge&goal=4000&settlement_date=2014-09-23&objection_period_secs=604800&accept_terms_of_service=current&use_existing=1"

レスポンス

{
    "activity": "running", 
    "created_datetime": "2014-09-05 04:56:45", 
    "evaluation_method": "ge", 
    "goal": "4000", 
    "human_resolution_scheduled_datetime": null, 
    "id": 376, 
    "is_user_authenticated": true, 
    "machine_resolution_scheduled_datetime": "2014-09-23 00:00:00", 
    "measurement": "total_distance", 
    "no_pubkey": "02397f0f5cefd6610a6aa722baf0462c5fa6e172b585784ad07644c29ece1fd06a", 
    "objection_fee_satoshis_due": 1000000, 
    "objection_fee_satoshis_paid": 0, 
    "objection_period_secs": 604800, 
    "settlement_date": "2014-09-23", 
    "source": "runkeeper", 
    "user_id": "29908850", 
    "user_name": "edochan", 
    "user_profile": "edochan", 
    "value": "4000", 
    "winner": "No", 
    "winner_privkey": "L38R5VMnFyGmAHLY6Cx25ky5QaeNN72QugPJXuCM4DJtTBt4hTuV", 
    "yes_pubkey": "0315796f27cc850af4ff89b39b32e571b1e40c005b9943c14ae192e25d0918c5cd"
}

machine_resolution_scheduled_datetimeは自動で事実認証する日時ですね。きっと。この情報は見たかったです。

user_nameが"edochan"になってます。カワイイな。

ソースコードを読む

契約のセットアップについて流れを見ていきます。

1.「Set this goal」ボタンクリック

<button id="set-goal-submit" type="submit" class="btn btn-primary set-goal-submit">Set this goal</button>

2.clickイベントハンドラ

$('#set-goal-submit').click( function() {
    register_contract();
    return false;
});

3.register_contractメソッド

  1. RKのRunkeeper API実行。レスポンスはdataに入る。
  2. P2SHアドレス作成。dataに格納する。
  3. シェア用のURL作成
  4. dataをwindow.localStorageに保存
  5. 契約データ追加(blockrから残高取得してdataに保存)
  6. 画面下部に契約情報追加(RKとblockrから情報を取り出す)
function register_contract() {
  $('body').addClass('registering-fact');
  var url = oracle_api_base + '/runkeeper/new';
  params = {
      'user_id': $('#user').val(),
      'activity': $('#activity').val(),
      'measurement': $('#measurement').val(),
      'goal': $('#goal').val(),
      'settlement_date': $('#settlement_date').val(),
      'comparison': 'ge',
      'objection_period_secs': (24*60*60),
      'accept_terms_of_service': 'current',
  };

  (省略)

  var user_pubkey = $('#public-key').text(); // 画面表示時に作成される

  (省略)

  $.ajax({
      url: url, 
      type: request_type,
      data: params,
      dataType: 'json', 
  }).done(function(data) {
      data['wins_on'] = wins_on;
      data['yes_user_pubkey'] = user_pubkey;
      data['no_user_pubkey'] = charity_pubkey;
      data['charity_display'] = charity_display;
      data['is_testnet'] = is_testnet;
      data['address'] = p2sh_address(data);

      var jump_to = '#' + sharing_url(data, false);
      document.location.hash = jump_to;
      $(document).scrollTop( $("#section3").offset().top );

      store_contract(data);
      reflect_contract_added(data);

  (省略)

BY MY COINの場合、自分との戦いなので、YESは常に自分のpubkeyになり、NOはチャリティのpubkeyになります。P2SHアドレスは後で書きます。

data['yes_user_pubkey'] = user_pubkey;
data['no_user_pubkey'] = charity_pubkey;
data['address'] = p2sh_address(data);

4.p2sh_addressメソッド (register_contractから呼ばれる)

bitcore.jsのbitcore.Address.fromScriptを使ってスクリプトを元にP2SHアドレスを作成しています。

function p2sh_address(data, include_user_keys) {

    if (include_user_keys == null) {
        include_user_keys = true;
    }

    var script = redeem_script(data);
    address_version = data['is_testnet'] ? 'testnet' : 'livenet';
    var addr = bitcore.Address.fromScript(script, address_version);
    return addr.toString();

}

5.redeem_scriptメソッド

bitcore.jsのTransactionBuilder.infoForP2shを使ってredeem_scriptを作成しています。(bitcore_monkey_patches.jsも使われています。詳細不明)

function redeem_script(data) {

    include_user_keys = true;

    var yes_pubkeys = [ data['yes_user_pubkey'], data['yes_pubkey'] ];
    var no_pubkeys = [ data['no_user_pubkey'], data['no_pubkey'] ];
    var user_pubkeys = [ data['yes_user_pubkey'], data['no_user_pubkey'] ];

    // multisig group p2sh
    var opts = {
        nreq: include_user_keys ? [2,2,2] : [2,2],
        pubkeys: include_user_keys ? [ yes_pubkeys, no_pubkeys, user_pubkeys ] : [ yes_pubkeys, no_pubkeys ]
    };

    var address_version = data['is_testnet'] ? 'testnet' : 'livenet';
    var info = TransactionBuilder.infoForP2sh(opts, address_version);
    var p2shScript = info.scriptBufHex;
    return p2shScript;
}

6.store_contractメソッド

window.localStorageに保存。キーは'contract_store'。

function store_contract(c, is_update) {

    contract_store = {
        'contracts': {},
        'default': null
    }

    p2sh_addr = c['address'];
    contract_json_str = localStorage.getItem('contract_store');
    if (contract_json_str) {
        contract_store = JSON.parse(contract_json_str);
    }
    if (!is_update) {
        if (contract_store['contracts'][p2sh_addr]) {
            return; // Already there
        }
    }
    contract_store['contracts'][p2sh_addr] = c;
    contract_store['default'] = p2sh_addr;

    localStorage.setItem('contract_store', JSON.stringify(contract_store));

}

ざっくりいうと以下情報を保存しています。

contract_store['contracts'][p2sh_addr] = c; // cはAPIレスポンス、画面の入力値など多数の情報。P2SHアドレス毎に保存
contract_store['default'] = p2sh_addr; // 使用用途不明

7.登録した契約情報に対して残高チェック

blockr.ioから残高をチェックします。登録時は残高0です。

function populate_contract_and_append_to_display(c) {

    var addr = c['address'];

    var url = c.is_testnet ? 'https://tbtc.blockr.io/api/v1/address/balance/' + addr+'?confirmations=0' : 'https://btc.blockr.io/api/v1/address/balance/' + addr+'?confirmations=0';
    $.ajax({
        url: url, 
        type: 'GET',
        dataType: 'json', 
        success: function(data) {
            c['balance'] = data['data']['balance'];
            populate_reality_key_and_append_to_display(c);
        },
        error: function(data) {
            c['load_error_funds'] = true;
            populate_reality_key_and_append_to_display(c);
        }
    });

}

8.RKにアクセスして情報を取得します。

function populate_reality_key_and_append_to_display(c) {

    var url = oracle_api_base + '/runkeeper/'+c['id']+'/'+oracle_param_string
    $.ajax({
        url: url, 
        type: 'GET',
        dataType: 'json', 
    }).fail( function(data) {
        c['load_error_fact'] = true;
    }).always( function(data) {
        $.extend(c, data);
        append_contract_to_display(c);
    });

}

9.画面に表示する項目を作成

APIと画面から取得した情報(パラメータc)を画面に表示します。 詳細は次回やりますが、claimボタンの実装もここに含まれています。

function append_contract_to_display(c) {

    var frm = $('#claim-form');

(省略)

まとめ

realitykeysdemo.pyと異なりブロードキャストしてません。BY MY COINは自分との戦いでありカウンターパーティリスク(Aliceに対するBobという意味で)が無いため、P2SHアドレスを作るときにFundする必要がないのだと思います。もちろん実装しだいでいくらでも変更できますね。

次回も引き続きソースコードを読みます。

参考

[iOS 7] JavaScriptCore Framework を使った Objective-C と JavaScript の連携ができるようになった

TransactionBuilder: A (hopefully) easy API to generate Bitcoin transactions

What is the HashToScriptMap

疑問(今度調べよう!)

  • RKはCounterpartyリスクですよね。仮の仮の話でRKが無くなった場合にどうなるか。どういう解決策があるか考えます。