Upgrade to Pro — share decks privately, control downloads, hide ads and more …

テスト駆動開発から証明駆動開発へ #JTF2019 / July Tech Festa 2019

y_taka_23
December 08, 2019

テスト駆動開発から証明駆動開発へ #JTF2019 / July Tech Festa 2019

July Tech Festa 2019 で使用したスライドです。

近年、テストを書く文化は広く普及しており、開発フローにおいて自動テストを組み込むことはもはや常識となりました。しかしよく考えてみると、有限個のテストケースが保証しているのは、所詮「特定の有限個の入力に対する出力」にしか過ぎません。では「あり得る全ての入力」に対してプログラムの性質を保証することは果たして可能でしょうか? この問いに対する答えのひとつが「定理証明」と呼ばれる手法です。定理証明では、数学的な「証明」をプログラム上でエンコードすることにより、真に「全ての入力」を扱うことができます。本セッションではこの定理証明を取り上げ、従来のテストとの考え方の違いや具体的な適用方法について、サンプルを交えつつ解説します。

イベント概要:https://2019.techfesta.jp/speakers#A10

y_taka_23

December 08, 2019
Tweet

More Decks by y_taka_23

Other Decks in Technology

Transcript

  1. #JTF2019 #JTF2019_A CloudNative の立役者 etcd • Kubernetes のバックエンド KVS ◦

    システム全体の Source of Truth ◦ 壊れると Kubernetes が動作不能に • 分散合意アルゴリズム Raft を利用 ◦ クラスタの過半数が生存していれば大丈夫 ◦ Paxos と比較して理解可能性を重視
  2. #JTF2019 #JTF2019_A etcd v3.4 (2019/08) での修正点 • 新ノードの Join 時に問題が発生

    ◦ データコピーの際、Leader が高負荷で死ぬ ◦ 起動失敗の場合でもメンバとして認識してしまう ◦ 1-Node クラスタへの Join に失敗すると即崩壊 • 新しい状態 Learner の導入 ◦ Join 直後は「過半数」にカウントされない
  3. #JTF2019 #JTF2019_A 分散システムの検証困難性 • 各ノードが非同期に動作する ◦ 考えるべきパターンが多すぎる • ノードやネットワークは信頼できない ◦

    独立にランダムなタイミングで故障が発生 • 仕様そのものが複雑になりがち ◦ 特定の瞬間ではなく一連の動作が問題に
  4. #JTF2019 #JTF2019_A 古典的なテストは「未知」に弱い • 人間がテストケースを与える ◦ 明示した有限個のケースしか保証できない ◦ 思いつかない異常系はそもそも考慮外 •

    再現性がある問題しか扱えない ◦ 「たまに落ちるテスト」はあまり役に立たない ◦ 特にタイミングに依存する問題は難しい
  5. #JTF2019 #JTF2019_A Chaos Engineering • 運用環境であえて障害を発生させる ◦ サービス不能状態に陥らないか ◦ 障害状態から正しく復旧できるか

    • 隠れていた問題点を炙り出せる ◦ 意図して発生させることが難しいバグを発見 ◦ いわば「未知に素早く反応する」アプローチ
  6. #JTF2019 #JTF2019_A Section 1 のポイント • 流行りの分散システムは留意点が多い ◦ タイミング・組み合わせ・ランダム性 •

    古典的なテストの限界 ◦ 既知の問題から外れたケースの保証は苦手 • 未知の事象にどう対処するか ◦ Chaos Engineering は「未知に素早く反応」する
  7. #JTF2019 #JTF2019_A 形式手法とは • システムを数学的対象により表現 ◦ その対象の性質に基づいて検証を行う ◦ 対象として何を選ぶかでツールの性質が決まる ◦

    テストケースの抜けや漏れが生じない • 厳密化した仕様を機械的にチェック ◦ いわば「既知の範囲を押し拡げる」アプローチ
  8. #JTF2019 #JTF2019_A 形式手法の分類 • モデル検査 ◦ システムが取りうる値を列挙して探索 ◦ 有限個のパターンに収まれば自動化できる •

    定理証明(今日のテーマ) ◦ いわゆる数学的な証明をプログラム的に表現 ◦ 真に無限個のパターンを扱うことができる
  9. #JTF2019 #JTF2019_A 静的型による安全性 • 型は値の不変条件を表す ◦ 実行中に int 型が string

    型に変わったりしない ◦ 指定した関数・メソッドの存在を保証 • 型システムは言語によって色々 ◦ 豊かな型システムは複雑な条件を強制できる ◦ 多くの場合、数学的な定式化が与えられている
  10. #JTF2019 #JTF2019_A 直和型 • ふたつの型のどちらか片方を持つ ◦ パターンマッチでもれなく場合分けできる type result =

    | Success of int | Error of string fun handle r = match r with | Success val -> do_something val | Error msg -> show_message msg
  11. #JTF2019 #JTF2019_A プログラムの世界 • P 型と Q 型の直積型 ◦ P

    型と Q 型の両方の値が手に入る • P 型と Q 型の直和型 ◦ P 型か Q 型の値の少なくとも片方は手に入る • P 型から Q 型への関数型 ◦ P 型の値を受け取って Q 型の値を作り出す
  12. #JTF2019 #JTF2019_A 証明の世界 • P かつ Q ◦ P と

    Q の両方の証明が手に入る • P または Q ◦ P か Q の証明の少なくとも片方は手に入る • P ならば Q ◦ P の証明を受け取って Q の証明が作り出す
  13. #JTF2019 #JTF2019_A プログラムの世界(再掲) • P 型と Q 型の直積型 ◦ P

    型と Q 型の両方の値が手に入る • P 型と Q 型の直和型 ◦ P 型か Q 型の値の少なくとも片方は手に入る • P 型から Q 型への関数型 ◦ P 型の値を受け取って Q 型の値を作り出す
  14. #JTF2019 #JTF2019_A Curry-Howard 同型対応 証明の世界 プログラムの世界 命題 型 命題 P

    の証明 P 型の値を得るプログラム P かつ Q P と Q の直積型 P または Q P と Q の直和型 P ならば Q P から Q への関数型
  15. #JTF2019 #JTF2019_A 例:三段論法 • 証明の世界 ◦ 前提:ソクラテスならば人間、人間ならば死ぬ、 ◦ 帰結:ソクラテスならば死ぬ •

    プログラムの世界 ◦ 実装済み:f : double -> int、g : int -> string ◦ 関数合成:x => g (f x) : double -> string
  16. #JTF2019 #JTF2019_A 例:Modus Ponens • 証明の世界 ◦ 前提:P ならば Q

    である、P は成り立つ ◦ 帰結:Q は成り立つ • プログラムの世界 ◦ 実装済み:f : int -> string、x : int ◦ 関数適用:f x : string
  17. #JTF2019 #JTF2019_A Section 2 のポイント • 形式手法とは ◦ システムを数学的な対象にマッピング ◦

    大きく分けてモデル検査と定理証明がある ◦ 静的型付き言語も広い意味で形式手法 • 命題とプログラムの型とが対応する ◦ 証明をプログラムにエンコードして扱える
  18. #JTF2019 #JTF2019_A 数学的帰納法 • 証明を 2 ステップに分割 ◦ 出発点となるケースについて成立を示す ◦

    任意の場所の「一歩手前」まで成立したと仮定 ◦ その仮定の下で「一歩先」での成立を示す • 無限性のある主張が証明できる ◦ 直接計算で「全ての自然数 n について」は不可能
  19. #JTF2019 #JTF2019_A Curry-Howard 同型対応ふたたび 証明の世界 プログラムの世界 命題 型 命題 P

    の証明 P 型の値を得るプログラム P かつ Q P と Q の直積型 P または Q P と Q の直和型 P ならば Q P から Q への関数型 数学的帰納法 再帰関数
  20. #JTF2019 #JTF2019_A Coq で和を計算する関数 Inductive nat : Type := |

    O : nat | S : nat -> nat. Fixpoint sum_upto (n : nat) : nat := match n with | O => 0 | S n’ => n + sum_upto n’ end. Compute (sum_upto 100). (* 5050 *)
  21. #JTF2019 #JTF2019_A Coq で和を計算する関数 Inductive nat : Type := |

    O : nat | S : nat -> nat. Fixpoint sum_upto (n : nat) : nat := match n with | O => 0 | S n’ => n + sum_upto n’ end. Compute (sum_upto 100). (* 5050 *) 自然数を直和型で定義
  22. #JTF2019 #JTF2019_A Coq で和を計算する関数 Inductive nat : Type := |

    O : nat | S : nat -> nat. Fixpoint sum_upto (n : nat) : nat := match n with | O => 0 | S n’ => n + sum_upto n’ end. Compute (sum_upto 100). (* 5050 *) パターンマッチして 再帰
  23. #JTF2019 #JTF2019_A Coq で証明すべき定理 Theorem sum_formula : forall n :

    nat, 2 * (sum_upto n) = n * S n. Proof. intro n. induction [| n’ H_n’ ] - (* n = 0 の場合の証明 *) ... - (* n = S n’ の場合の証明 *) ... Qed.
  24. #JTF2019 #JTF2019_A Coq で証明すべき定理 Theorem sum_formula : forall n :

    nat, 2 * (sum_upto n) = n * S n. Proof. intro n. induction [| n’ H_n’ ] - (* n = 0 の場合の証明 *) ... - (* n = S n’ の場合の証明 *) ... Qed. パターンマッチに よる場合分け
  25. #JTF2019 #JTF2019_A Program Extraction • Coq 単体では I/O などが記述できない ◦

    関数の性質を証明しても実装に組み込めない • 通常の言語のコードに変換できる ◦ 重要な関数のみ証明し、変換して使用する ◦ デフォルトでは OCaml、Haskell、Scheme ◦ プラグインで拡張することも可能
  26. #JTF2019 #JTF2019_A Section 3 のポイント • 数学的帰納法の利用 ◦ プログラム的には再帰関数として実現できる •

    Coq による対話的証明 ◦ 書き換えによりゴールを引き寄せて進む • Extraction によるコード生成 ◦ 重要な関数を証明してプロダクトに埋め込む
  27. #JTF2019 #JTF2019_A 例:分散ロックサーバ • サーバとエージェントが存在 ◦ エージェントは LockMsg / UnlockMsg

    を要求 ◦ サーバは LockMsg を先着順で処理 ◦ ロックを与える場合は GrantMsg を送信 • 二重ロックを防ぎたい ◦ 非同期リクエストでも正しく処理できるか?
  28. #JTF2019 #JTF2019_A 証明フレームワーク Verdi • Coq 用のフレームワーク ◦ 分散システムを体系的に証明できる ◦

    Extraction してランタイムに組み込み可能 • ユーザが定義するシステムの動作 ◦ ノード、外部入出力、内部メッセージ ◦ 外部入力、内部メッセージに対するハンドラ
  29. #JTF2019 #JTF2019_A Server Agent (A1) Other Process Agent (A2) Other

    Process Client Client 外部入出力 内部メッセージ
  30. #JTF2019 #JTF2019_A 外部入力ハンドラ(エージェント) HandleInp (n: Name) (s: State n) (inp:

    Inp) := match n with | Server => nop | Agent n => match inp with | Lock => send (Server, LockMsg) | Unlock => if s == true then ( s := false;; send (Server, UnlockMsg))
  31. #JTF2019 #JTF2019_A 外部入力ハンドラ(エージェント) HandleInp (n: Name) (s: State n) (inp:

    Inp) := match n with | Server => nop | Agent n => match inp with | Lock => send (Server, LockMsg) | Unlock => if s == true then ( s := false;; send (Server, UnlockMsg)) Lock が来たら サーバに LockMsg
  32. #JTF2019 #JTF2019_A 外部入力ハンドラ(エージェント) HandleInp (n: Name) (s: State n) (inp:

    Inp) := match n with | Server => nop | Agent n => match inp with | Lock => send (Server, LockMsg) | Unlock => if s == true then ( s := false;; send (Server, UnlockMsg)) Unlock が来たら 自分の持つフラグを下ろして サーバに UnlockMsg
  33. #JTF2019 #JTF2019_A メッセージハンドラ(サーバ) HandleMsg (n: Name) (s: State n) (src:

    Name)(msg: Msg) := match n with | Server => nop match msg with | LockMsg => if s == [] then send (src, GrantMsg);; s := s ++ [src] | UnlockMsg => s := tail s;; if s != [] then send (head s, GrantMsg)
  34. #JTF2019 #JTF2019_A メッセージハンドラ(サーバ) HandleMsg (n: Name) (s: State n) (src:

    Name)(msg: Msg) := match n with | Server => nop match msg with | LockMsg => if s == [] then send (src, GrantMsg);; s := s ++ [src] | UnlockMsg => s := tail s;; if s != [] then send (head s, GrantMsg) リストの先頭が ロック保持中のエージェント
  35. #JTF2019 #JTF2019_A メッセージハンドラ(エージェント) HandleMsg (n: Name) (s: State n) (src:

    Name)(msg: Msg) := match n with | Agent n => match msg with | GrantMsg => s := true;; output Grant
  36. #JTF2019 #JTF2019_A メッセージハンドラ(エージェント) HandleMsg (n: Name) (s: State n) (src:

    Name)(msg: Msg) := match n with | Agent n => match msg with | GrantMsg => s := true;; output Grant サーバから返事が来たら 自分の持つフラグを立てて 外部システムに Grant 通知
  37. #JTF2019 #JTF2019_A Verdi によるシステムの定式化 • 三つ組 (Σ, P, T) で「世界」を定義

    ◦ Σ : 各ノードが持つ内部状態 ◦ P : 通信路の途中にいるパケットの多重集合 ◦ T : 外部入出力イベントの列(トレース) • 世界間の到達可能関係を考える ◦ システムの挙動はグラフ構造として表される
  38. #JTF2019 #JTF2019_A Σ = { Server : [], A1: false,

    A2: false } P = { (A1, LockMsg), (A2, LockMsg)} T = [ <A1, Lock>, <A2, Lock> ] Σ = { Server : [], A1: false, A2: false } P = { (A1, LockMsg) } T = [ <A1, Lock> ] Σ = { Server : [], A1: false, A2: false } P = {} T = []
  39. #JTF2019 #JTF2019_A Σ = { Server : [], A1: false,

    A2: false } P = { (A1, LockMsg), (A2, LockMsg)} T = [ <A1, Lock>, <A2, Lock> ] Σ = { Server : [], A1: false, A2: false } P = { (A1, LockMsg) } T = [ <A1, Lock> ] Σ = { Server : [], A1: false, A2: false } P = {} T = [] A1 が Lock を要求
  40. #JTF2019 #JTF2019_A Σ = { Server : [], A1: false,

    A2: false } P = { (A1, LockMsg), (A2, LockMsg)} T = [ <A1, Lock>, <A2, Lock> ] Σ = { Server : [], A1: false, A2: false } P = { (A1, LockMsg) } T = [ <A1, Lock> ] Σ = { Server : [], A1: false, A2: false } P = {} T = [] A2 が Lock を要求
  41. #JTF2019 #JTF2019_A Σ = { Server : [A1, A2], A1:

    false, A2: false } P = {} T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: true, A2: false } P = { (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: false, A2: false } P = { (A1, GrantMsg), (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock> ] Server が A1 を Grant
  42. #JTF2019 #JTF2019_A Σ = { Server : [A1, A2], A1:

    false, A2: false } P = {} T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: true, A2: false } P = { (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: false, A2: false } P = { (A1, GrantMsg), (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock> ] A1 がGrant を受信
  43. #JTF2019 #JTF2019_A Σ = { Server : [A1, A2], A1:

    false, A2: false } P = {} T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: true, A2: false } P = { (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ] Σ = { Server : [A1], A1: false, A2: false } P = { (A1, GrantMsg), (A2, LockMsg) } T = [ <A1, Lock>, <A2, Lock> ] Server が A2 の要求を認識
  44. #JTF2019 #JTF2019_A Σ = { Server : [A2], A1: false,

    A2: true } P = { () } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock>, <A2, Grant> ] Σ = { Server : [A2], A1: false, A2: false } P = { (A2, GrantMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock> ] Σ = { Server : [A1, A2], A1: true, A2: false } P = { (A1, UnlockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock> ] A1 が Unlock を要求
  45. #JTF2019 #JTF2019_A Σ = { Server : [A2], A1: false,

    A2: true } P = { () } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock>, <A2, Grant> ] Σ = { Server : [A2], A1: false, A2: false } P = { (A2, GrantMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock> ] Σ = { Server : [A1, A2], A1: true, A2: false } P = { (A1, UnlockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock> ] Server が A2 を Grant
  46. #JTF2019 #JTF2019_A Σ = { Server : [A2], A1: false,

    A2: true } P = { () } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock>, <A2, Grant> ] Σ = { Server : [A2], A1: false, A2: false } P = { (A2, GrantMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant>, <A1, Unlock> ] Σ = { Server : [A1, A2], A1: true, A2: false } P = { (A1, UnlockMsg) } T = [ <A1, Lock>, <A2, Lock>, <A1, Grant> ,<A1, Unlock> ] A2 が Grant を受信
  47. #JTF2019 #JTF2019_A Verdi による仕様記述 • トレース T に対して仕様を定義 ◦ システムの内部メッセージには言及しない

    • 例:二重ロックしない ◦ 任意の a1, a2 を考えたとき、 T = t1 ++ [<a1, Grant>] ++ t2 ++ [<a2, Grant>] ++ t3 の形で表せるなら、t2 は <a1, Unlock> を含む
  48. #JTF2019 #JTF2019_A 形式手法の分類(再掲) • モデル検査 ◦ システムが取りうる値を列挙して探索 ◦ 有限個のパターンに収まれば自動化できる •

    定理証明 ◦ いわゆる数学的な証明をプログラム的に表現 ◦ 真に無限個のパターンを扱うことができる
  49. #JTF2019 #JTF2019_A 参考:モデル検査の場合 • 世界の遷移が作るグラフに対して探索 ◦ 条件を満たさない T を持つ世界を探す ◦

    世界が有限個になるようにモデル化する必要 Initial World World World World World World T が不正 World World World World
  50. #JTF2019 #JTF2019_A 例:メッセージ重複への対処 • Verdi が自動でラッパを適用 ◦ メッセージにユニークな ID 番号を振る

    ◦ 各ノードに「すでに読んだ ID」を記憶させる ◦ すでに読んだ ID のメッセージは無視する • 二重ロック禁止の仕様は守れるか? ◦ 重複なしの場合の証明が成り立たない可能性
  51. #JTF2019 #JTF2019_A Σ = { Server : [], read: {

    000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] Σ = { Server : [], read: { 000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] 重複ありの場合 重複なしの場合
  52. #JTF2019 #JTF2019_A Σ = { Server : [], read: {

    000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] Σ = { Server : [], read: { 000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] Σ = { Server : [] A1: false A2: false } P = { (A1, LockMsg), } T = [ ..., <A1, Lock> ] 重複ありの場合 重複なしの場合 unwrap はトレース T を保存
  53. #JTF2019 #JTF2019_A Σ = { Server : [], read: {

    000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] Σ = { Server : [], read: { 000, … } A1: false, read: { 001, … } A2: false, read: { 002, … } } P = { (042, A1, LockMsg), (042, A1, LockMsg), } T = [ ..., <A1, Lock> ] Σ = { Server : [] A1: false A2: false } P = { (A1, LockMsg), } T = [ ..., <A1, Lock> ] 重複ありの場合 重複なしの場合 unwrap 先でも 対応する(自明な) 遷移が存在
  54. #JTF2019 #JTF2019_A Initial World 重複ありの場合 重複なしの場合 証明済: 到達可能な任意の世界で トレース T

    は条件を満たす World World World Initial World World World World World 対応する遷移が存在 = ラップした状態でも 到達可能性は変わらない
  55. #JTF2019 #JTF2019_A Initial World 重複ありの場合 重複なしの場合 証明済: 到達可能な任意の世界で トレース T

    は条件を満たす World World World Initial World World World World World 実は読み替え可能: 到達可能な任意の世界で トレース T は条件を満たす
  56. #JTF2019 #JTF2019_A Verified System Transformer • より複雑な意味論への変換 ◦ メッセージとハンドラを自動でラップ ◦

    故障への対応について改めて考える必要がない • 実装だけでなく証明も変換可能 ◦ 意味論の間にある模倣関係を利用 ◦ 仕様をトレースに制限したのが効いている
  57. #JTF2019 #JTF2019_A 故障を含む Verdi の意味論(一部) • Duplicating Semantics ◦ メッセージが通信路上で複製される

    • Dropping Semantics ◦ メッセージが消失、タイムアウトする • Node Failure ◦ ノードが落ちたり、勝手に復活したりする
  58. #JTF2019 #JTF2019_A Section 4 のポイント • Verdi による分散システムの証明 ◦ Runtime

    とセットで実行可能コードを生成 • システムのトレースで仕様を表現 ◦ 複雑に見えるが本質は数学的帰納法 • Verified System Transformer ◦ 故障に対応したバージョンに変換できる
  59. #JTF2019 #JTF2019_A 本日のまとめ • 形式手法による検証 ◦ テストでは扱いづらい分散システムにも使える • Coq による定理証明

    ◦ プログラムと証明との Curry-Howard 対応 • 証明フレームワーク Verdi ◦ 分散システムの証明が可能、Raft も証明済み