LoginSignup
61
37

More than 5 years have passed since last update.

Block Chain を Rust で書きながら、労働時間管理できないか挑戦した話 (前編)

Last updated at Posted at 2017-12-28

Block Chain は書き換えができない台帳 と聞いて真っ先に思いついたのが、労働時間の管理

やりたい事

誰が雇用者で誰が被雇用者とか抜きにして、A は B の為に X 時間労働しましたよ といった記録を台帳に記していく。

雇用者も被雇用者も改ざんできない労働管理台帳を作れないか。

ということで、これを作ったらどんな感じになるのか試してみた。

機能要件

Block Chain

まずは、分かりやすい Block Chain の解説

5分でわかるブロックチェーンの基本的な仕組み

● トランザクション

冒頭に述べた通り、誰が誰に何時間与えるのか、を記録していく。

  • Sender : 被雇用者
  • Recipient : 雇用者
  • Amount : 時間

本当は、雇用者 / 被雇用者 間の合意部分も含めたいが、やり過ぎになりそうなので今回は合意後の記録だけをする。

● コンセンサスアルゴリズム

一番ノーマルな Proof Of Work。

● マイニング報酬

悩みどころ。
お金の場合、それ自体を与えることに価値があるが、時間は貰っても別に嬉しくない。

時間が貨幣とトレードできるとなるとインセンティブ働くけど、そうすると結局新たな仮想通貨一つ作っただけのような気もして。

ネットワーク

本格的な P2P ネットワークを構築するのは盛り込み過ぎなのでスコープ外として、全体を覆う薄い管理層と個別にデータをやりあう直接接続層と分けて考える。

とは言え、中央集権的過ぎると意味が無いので、本当に必要な最低限の情報のみ管理層に任せる。

管理層

全ノードのアドレスや現在の状態 ( 接続可 / 不可 ) だけを 管理する。

直接接続層

管理層の情報を元に接続を確立させた後は、データ自体は直接送り合う。

システム要件

まずは、ネットワークとしては

  • 管理層 : Firebase + Firestore
  • 直接接続層 : WebRTC

を考えている。

Block Chain のコア部分は Rust + wasm とする。

開発

  • Windows 10
  • Rust - 1.24.0-nightly
  • Cargo - 0.25.0-nightly
  • Chrome 64 bit - 63.0

では、以下順番に見ていく。

Rust セットアップ

以下、 scoop は使えるものとして進める

まずは、rustup を入れて、nightly ツールチェインを入れる。

PS> scoop install rustup
PS> rustup toolchain install nightly
PS> rustup default nightly

次に、wasm としてビルド出来るように target を追加する。
以下を参考に。

Rust for the Web

PS> rustup target add wasm32-unknown-unknown --toolchain nightly
PS> cargo install --git https://github.com/alexcrichton/wasm-gc

これで、wasm のビルドはできるようになった。

Cargo プロジェクト作成

次に、gh コマンド で Github プロジェクトを作り、Cargo プロジェクトとして初期化する。

PS> gh create

# create kentork/honest project

PS> gh cd kentork/honest
PS> cargo init --lib .
├── ./Cargo.toml
├── ./gitignore
├── ./LICENSE.txt
├── ./README.md
└── ./src
    └── lib.rs

Cargo の設定を少し変更する。

Cargo.toml
[package]
name = "honest"
version = "0.1.0"
authors = ["kentork <4198504+kentork@users.noreply.github.com>"]

[dependencies]

[lib]
crate-type=["cdylib"]   # ← これポイント

動作テスト

lib.rs
// ↓ これがあると、 Javascript から 'add_one' シンボルで呼べる
#[no_mangle]  
pub fn add_one(x: i32) -> i32 {
   x + 1
}
PS> cargo +nightly build --target wasm32-unknown-unknown --release
PS>
PS> mkdir -p ./public/wasm
PS> wasm-gc ./target/wasm32-unknown-unknown/release/honest.wasm ./public/wasm/honest.wasm
PS>
PS> micro ./public/index.html
./public/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <script>
    fetch('wasm/honest.wasm')
      .then((response) => response.arrayBuffer())
      .then((bytes) => WebAssembly.instantiate(bytes, {}))
      .then((results) => {
        const instance = results.instance;
        const add_one= instance.exports.add_one;
        console.log(add_one(5));
      });
  </script>
</head>

<body>
</body>

</html>

適当にサーバを立てて

PS> cd public
PS> caddy

ブラウザで Console に 6 が出れば完了。
( release ビルドしないと、呼べるけど結果が違ったりする現象に遭遇、理由は不明。)

Cargo build のパラメータ多いので、ビルドスクリプトを適当に書く。

build.ps1
cargo +nightly build --target wasm32-unknown-unknown --release

if ($? -eq $true) {
  wasm-gc ./target/wasm32-unknown-unknown/release/honest.wasm ./public/wasm/honest.wasm
}

Block Chain 部実装

では、Block Chain 部分を実装していく。
プロジェクトは以下。

コア部分

Block Chain のコア部分については、以下記事をベースに進めていく。
Python で書かれているので、とても分かりやすく移植もしやすい。

[翻訳記事] ブロックチェーンを作ることで学ぶ 〜ブロックチェーンがどのように動いているのか学ぶ最速の方法は作ってみることだ〜

また、その他の実装と見比べながらいいとこ取りしていく。

A blockchain in 200 lines of code

◆ Block, Transaction 周り

Block Chain では、取引情報である Transactionと、それを複数含めた Block で構成される。

Transaction

送信者(sender) と受信者(recipient) と取引量(amount) で構成される。

transaction.rs
pub struct Transaction {
  pub sender: String,
  pub recipient: String,
  pub amount: u32,
  // pub signature: String,
}

本当は、取引内容の改ざん防止と身元証明として電子署名をしたかったが、諸事情により未実装

Block

まず、Transaction の配列を保持している。
また、最初の Block からの Index と、前回 Block のハッシュ値である Previous Hash が含まれる。

Index:1 の Block 全体をハッシュ化した物を Index:2 が保持し、
Index:2 の Block 全体をハッシュ化した物を Index:3 が保持し、...
と続いていく。

Timestamp は、一応含めてるけどまだ使ったこと無い。

で、ポイントとなるのが proof というやつで、これが偽造を防止している。
詳細は後述。

block.rs
pub struct Block {
  pub index: u64,
  pub timestamp: u64,
  pub transactions: Vec<Transaction>,
  pub proof: u64,
  pub previous_hash: String,
}

BlockChain

Block Chain 全体を管理するための BlockChain 構造体。

Block の連なりを 保存する Blocks と、まだBlock 化されていない Transaction を管理する Transactions がある。

Identifier はユーザ識別子で、Password と Passphrase から生成される。

new_transaction メソッド、new_block メソッド共にそのままの意味である。

BlockChain.rs
pub struct BlockChain {
  identifier: String,
  blocks: Vec<Block>,
  current_transactions: Vec<Transaction>,
}
impl BlockChain {
  pub fn identify(&mut self, password: &str, passphrase: &str) -> String {
    self.identifier = identification::generate(&password, &passphrase);
    self.identifier.clone()
  }

  pub fn new_transaction(&mut self, sender: &str, recipient: &str, amount: u32) -> u64 {
    self.current_transactions.push(Transaction {
      sender: sender.to_string(),
      recipient: recipient.to_string(),
      amount: amount,
    });
    self.blocks.len() as u64
  }

  fn new_block(&mut self, timestamp: u64, proof: u64) -> u64 {
    let current_index = self.blocks.len() as u64;
    let next_transactions = self.current_transactions.to_vec();

    let next = match self.blocks.last() {
      Some(previous) => {
        Block {
          index: current_index,
          timestamp: timestamp,
          proof: proof,
          previous_hash: previous.hash(),
          transactions: next_transactions
        }
      }
      None => {
        Block {
          index: 0,
          timestamp: timestamp,
          proof: proof,
          previous_hash: "genesis".to_string(),
          transactions: Vec::new(),
        }
      }
    };
    self.blocks.push(next);
    self.current_transactions = Vec::new();

    (self.blocks.len() - 1) as u64
  }
}

これで、基本部分は完成。

◆ Consensus アルゴリズム

これは、分散化されて保存されている Block Chain の中で、どれが正しい Block Chain なのかを決める過程。

採用したのは、Bitcoin と同じ Proof Of Work。


( これは例え話です )

我々は アイアンマン というドラマを制作している。
製作メンバーは匿名で集まった有志達で、ドラマは匿名公開ができる BlockToube というサイトで公開している。
特徴的なのが、ストーリー展開は視聴者の要望を集め、それを反映する形で決められていくということ。

公開は 6 話まで進み、視聴者の評判も良かった。

ところがそんなある日、制作メンバーの一部にそのストーリー展開に納得が行かないと言い出すものが現れ、あろうことか独自にストーリーに手を加えた アイアソマソ 第 6' 話 を勝手に作り出し BlockToube に公開してしまった。

視聴者は突然現れた 2 つのアイアンマンに困惑した。
悩んだ末、 とにかく先に公開された方を本物として見ようぜ ということになった。

その為、アイアソマソ製作サイドは急いでドラマの続きを作ろうと奮闘する。
しかし、BlockToube は独自のエンコードでなければ配信ができない仕様となっており、そのエンコード作業にとても時間がかかる

アイアソマソ製作メンバーは正規メンバーよりも数が少なく、全員でマシンフル回転エンコードしても正規版に追いつけない。
そうこうしている間にも、正規版は第 7 話 第 8 話と次々公開されてていく。

数日後、アイアソマソチームは解散した。


大体こんな感じで改ざんを防いでいるらしい。

ここでのポイントは 2つ

  1. 次の動画を作るのには 膨大なマシンリソースが必要
  2. 一番長い シリーズが信頼される

1. 膨大なマシンリソースが必要な処理 → Nonce 探し

このマシンリソースが必要な重い処理として Nonce 探し をさせる。

やっていることは、単純で、

Hash( Previous Block Hash + X) = 0000000xxxxxxxx... (x : SomeCharactor)

を満たす X を見つけること。
Hash 関数は一方向で法則性も無いため、X は総当たりで探すしかない。

ハッシュ値の頭に 0 を何個必要とするかによって難易度が変更でき、ここを調整することで全体のマシンリソース総量が増加してもブロック生成時間を一定に保てるらしい。

また、本当に正しいのかを他人が検証できなければ意味が無いため、発見は難しいが検証は簡単 である必要があるが、Nonce は Hash 関数にかけると直ぐ分かるのでこれを満たしている。

今回はサンプルと同じ、前回 Nonce と足し合わせてハッシュ化するシンプルなアルゴリズムとした。

Hash( Precious Nonce + Current Nonce) = 000xxxxxxxxxxxx...
nonce.rs
impl Nonce {
  pub fn find_next(&self) -> u64 {
    let mut nonce = 1;
    while !Nonce::verify(self.current, nonce) {
      nonce += 1;
    }
    nonce
  }

  pub fn verify(current: u64, next: u64) -> bool {
    let message = format!("{}{}", current, next);
    let digest = hash::sha256::digest(&message);
    digest.starts_with("000")
  }
}

新しい Block を生成する時には Nonce を含める必要があるので、この作業を行わなければならない。
その作業をいわゆる Mining と呼ぶ。

blockchain.rs
impl BlockChain {
 ...
  pub fn mine(&mut self) -> u64 {
    let recipient = self.identifier.to_string();
    self.new_transaction("0", &recipient, 2);

    let current_proof = self.blocks.last().unwrap().proof;
    let nonce = Nonce { current: current_proof };
    let next_proof = nonce.find_next();
    self.new_block(unixtime::nano::now(), next_proof);

    self.blocks.len() as u64
  }
 ...
}

悲しいかな、

2. 一番長いものが信頼される → Resolve

ルールは簡単。あとは、どうやってそれを実現するか。

● ネットワークに参加した時

ネットワーク参加時は Block Chain を持っていないので、取り敢えず全員の Block Chain を頂く。

ノード管理は Javascript 側でするので、Rust では

  • check_latest - 相手の最新ブロックが自分より長いか
  • receive_blocks - 全部貰って、検証して良ければ取り込む

を行う。

分岐点からの差分更新なども効率化する上では必要だろうが、今は簡易的に全部更新する。

blockchain.rs
impl BlockChain {
 ...
  pub fn check_latest(&self, another: &str) -> bool {
    let another_block = serializer::block::deserialize(another);

    if self.blocks.last().unwrap().index < another_block.index {
      true
    } else {
      false
    }
  }

  pub fn receive_blocks(&mut self, another: &str) -> bool {
    let another_chains = serializer::chain::deserialize(another);

    if self.blocks.len() < another_chains.len() && consensus::verify_chains(&another_chains) {
      self.blocks = another_chains;
      true
    } else{
      false
    }
  }
 ...
}

検証は以下

consensus.rs
pub fn verify_chains(chains: &Vec<Block>) -> bool {
  let mut iterator = chains.iter();

  match iterator.next() {
    Some(first) => {
      let mut _index = first.index;
      let mut _hash = first.hash();
      let mut _nonce = first.proof;

      iterator
        .map(|block| match block {
          &Block {
            index,
            ref previous_hash,
            proof,
            ..
          } if (_index + 1) == index && _hash == *previous_hash && Nonce::verify(_nonce, proof) =>
          {
            _index = index;
            _hash = block.hash();
            _nonce = proof;
            true
          }
          _ => false,
        })
        .all(|x| x)
    }
    None => false,
  }
}

● ネットワークに誰かが参加した時

今度は、自分が持っている Block 情報を誰かに返す方。

  • send_latest - 最新ブロックを送る
  • send_chain- 全部送る
blockchain.rs
impl BlockChain {
 ...
  pub fn send_latest(&self) -> String {
    serializer::block::serialize(&self.blocks.last().unwrap())
  }

  pub fn send_chain(&self) -> String {
    serializer::chain::serialize(&self.blocks)
  }
 ...
}

● 新しい Block を作ったら全員に知らせる

これ自体は、 Mining 成功を受けて Javascript 側がトリガーする。

前半まとめ

これでひとまず Core 部分はできた。
しかし、分からない点がいくつもある。

  • Mining 中の Transaction はどうなるのか。
  • Transaction の偽造はできないのか (署名もできないし偽造できるんじゃないの)。
  • ユーザが全員いなくなったら、台帳データは消えるけど。
  • そして、本当にこれだけで改ざんできない台帳ができるのか

後半へ続く。

( Rust も Block Chain もよく知らないまま見切り発車したため時間がかかった。後編はサクッとやりたい。 )

引っかかりポイント

Rust

この規模のアプリケーションを作るのは初めてだったが、やはり色々と引っかかった。

◆ 使えない Crate

一部の Crate が、target wasm32-unknown-unknown では使えなかった。

以下の変更で対応した。

  • rust-crypto → sha2
  • chrono → std::time を直接使う
  • ring → 変わりがなく、困っている
    • 電子署名ができないのは痛い
  • argon2rs → sha2 で代替している

全部の機能がブラウザ上で動く訳ないので当然と言えば当然だけど、知らずに引っかかるとダメージでかい。

argon2rs 以外はビルドで失敗したのでまだ良いが、argon2rs は使うまで気付かなかった
Rust のテストでは問題ないけど wasm 化したら上手くいかないとか、えらい爆弾抱えてて辛い。

◆ やっぱり所有権

Rust の表現力は高く素晴らしい事は分かったが、少し凝ったことすると途端に所有権が訳の分からない事になった。基礎から勉強し直したい。

● iterator と match

以下は実際に動作しているコードだが、これだけ書くのに数時間かかっている。
一応はこれで納得しているが保守できる自信ない。

と言うか、未だにスタイルに馴染めないでいる。
for と if とローカル変数と unwrap 使いまくるのが本当は正しいのかな。

pub fn verify_chains(chains: &Vec<Block>) -> bool {
  let mut iterator = chains.iter();

  match iterator.next() {
    Some(first) => {
      let mut _index = first.index;
      let mut _hash = first.hash();
      let mut _nonce = first.proof;

      iterator
        .map(|block| match block {
          // guard 付き pattern match と struct binding が一緒に使えたり使えなかったり。謎。
          &Block {
            index,
            ref previous_hash,    // ref は OK, & は NG。何故?
            proof,
            ..
          } if (_index + 1) == index && _hash == *previous_hash && Nonce::verify(_nonce, proof) =>
          {
            _index = index;
            _hash = block.hash();
            _nonce = proof;
            true
          }
          _ => false,
        })
        .all(|x| x)
    }
    None => false,
  }
}

● Generic な Deserialize

Deserialize して復元した struct の lifetime がこの中に閉じ込められるのは何となく理解できるが、それをどうしても外に出せなかった。

結局、struct 毎に分けて個別実装した。
この辺は勉強不足が元なので、もっと勉強したい。

use bincode;
use base64::{decode, encode};
use serde;

pub fn serialize<T: serde::Serialize>(data: &T) -> String {
  let bytes = bincode::serialize(&data, bincode::Infinite).unwrap();
  encode(&bytes)
}
pub fn deserialize<'a, T: serde::Deserialize<'a>>(data: &str) -> T {
  let bytes = decode(&data).unwrap().to_owned();
  let decoded: T = bincode::deserialize(&bytes).unwrap();
  // `bytes` does not live long enough
  decoded
}

WASM

◆ JavaScript との文字列のやり取り

文字列を受け取る/返すのが結構大変。
公式にサンプルがあるので、それを真似してみてようやくできた。

Hello Rust Demo - Calculate the SHA1 hash of input

Rust 側で alloc / dealloc な関数を公開するのは分かるとして、Javascript 側で newString とか copyCStr とか知らない関数使って進めてて、困ったなぁと思っていたら、どうやら bundle.js というファイルに Utility 関数が含まれていてそれを使っているらしい。

ちなみに、このファイルの中に fetchAndInstantiate という wasm 落としてくる手間をちょっと省ける関数もあったりする。

lib.rs

use std::mem;
use std::ffi::CString;
use std::os::raw::{c_char, c_void};
...


#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut c_void {
  let mut buf = Vec::with_capacity(size);
  let ptr = buf.as_mut_ptr();
  mem::forget(buf);
  return ptr as *mut c_void;
}
#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut c_void, cap: usize) {
  unsafe  {
    let _buf = Vec::from_raw_parts(ptr, 0, cap);
  }
}
#[no_mangle]
pub extern "C" fn dealloc_str(ptr: *mut c_char) {
    unsafe {
        let _ = CString::from_raw(ptr);
    }
}


#[no_mangle]
pub fn identify(user: &str, passphrase: &str) -> *mut c_char {
  let identification = BLOCK_CHAIN.lock().unwrap().identify(user, passphrase);
  let identification_str = identification.to_string();

  let c_string = CString::new(identification_str).unwrap();
  c_string.into_raw()
}
index.html
  <script src="bundle.js"></script>
  <script>
    fetchAndInstantiate('build/main.wasm')
    .then(mod => {
        exports = mod.exports;

        let outptr = exports.identify(
            newString(exports, "user"),
            newString(exports, "passphrase")
        );
        console.log(copyCStr(exports, outptr));
      });
  </script>

これで、コンソールに 4678dbce2833cc05f93afc7a90d75870d8713bda230bab2e75abfcc17b076ea1 が出れば取れてる。

◆ エラー処理

image.png

これは、argon2rs の Encoded::default2i を使った時のエラー。
全く分からない。

方法を調査中。

[12/31追記]

wasm-function と stack traces に出ていたのは、 release ビルドした事によってデバッグ情報が含まれていないだけだった。
以下オプションを付けたら、多分 mangling で変換されてはいるが予測可能な関数名が見えるようになった。

$env:RUSTFLAGS = "-g"    # flag を追加
cargo +nightly build --target wasm32-unknown-unknown --release

if ($? -eq $true) {
  wasm-gc ./target/wasm32-unknown-unknown/release/honest.wasm ./public/wasm/honest.wasm
}

image.png

諦めて emscripten に移行しようと思ったが、もう少し wasm32-unknown-unknown で粘ってみる。

61
37
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
37