ブログ > 2022 > January > Plutusバリデーター用のシンプルなプロパティベーステスト

Plutusバリデーター用のシンプルなプロパティベーステスト

cooked-validatorsライブラリーでオフチェーンコードを作成し、無料でプロパティベーステストを実行する方法

2022年 1月 27日 Victor Cacciari Miraldo 15 分で読めます

Plutusバリデーター用のシンプルなプロパティベーステスト

先日、Tweagでスマートコントラクトの検証と監査を担うチームを率いるVictor Miraldo氏に、分散型金融(DeFi)界で安全面からみた検証の重要性について話を伺いました。Victorは、Haskellおよび形式手法エンジニアで、ツールやプロセスを使った分散型アプリケーション(DApp)の安全性と正確性の確保に取り組んでいます。このブログでVictorは、単にDAppを作成し、デプロイするだけでは十分ではなく、すべての開発者は、さまざまな悪質なユーザーを想定してオンチェーンコードとPlutusスクリプトを徹底的にテストするべきである理由を述べています。このため、ここではTweagが開発したcooked-validatorsと呼ばれるPlutusバリデータースクリプトを使用するための既成ツールのライブラリーを紹介します。このライブラリーは、トランザクションの生成と送信を担当するオフチェーンコードの最深レイヤーの実装をサポートします。このライブラリーを使うと、トランザクションレベルでプロパティベースのテストを無料で実行できます。

早速本ライブラリーの使用について、Victorの話を聞いてみましょう。

はじめに

トランザクションレベルのテストにより、任意のトランザクションをバリデータースクリプトに送信し、その動作を評価することができます。このプロセスは、コントラクトのオフチェーンコード部分として、定義されたエンドポイントを使用するスマートコントラクト全体のテストとは異なります。オフチェーンコードはオンチェーンコードとシームレスに作用するよう設計されており、独自のセキュリティおよびセーフティチェックを内蔵しています。このメソッドは通常の操作では機能しますが、テスト環境では、オンチェーンバリデータースクリプトを不正な入力や悪質な入力から保護してしまうことが頻繁に起こります。ここでは、トランザクションレベルのテストのために、オフチェーンコードのサニタイジングを回避し、攻撃者が手作りしたオフチェーンインフラが持ち込む可能性のあるものと同じものでオンチェーンスクリプトを叩く必要があります。Webサービスになぞらえるなら、クライアントのユーザーインターフェイスで許可されている要求に加えて、任意の要求を送信してサーバーをテストすることに似ています。

cooked-validatorsライブラリーを使用すると、トランザクションの生成と送信を担当するオフチェーンコードを作成し、その同じコードを使用してトランザクションレベルでコントラクトの実行とテストを行うことができます。これにより、不具合が多数発生する可能性があるかどうかを検出するオンチェーンのテストがより簡単に作成できます。

cooked-validatorsライブラリーについて

cooked-validatorsを使用したコントラクトの作成は、既に使用しているContractモナドとさほど変わりません。分割契約に関するチュートリアルに従って「バリデータースクリプトの定義」まで終えると、コントラクトのオンチェーン部分を実行するsplitValidator関数が得られます。チュートリアルに従って進まなければ、splitValidatorコントラクトは一定の資金をロックし、ロックは事前に指定した2つの当事者間で資金を分割しない限り解除されません。

コントラクト自体とやり取りするためには、必要なトランザクションを作成しブロックチェーンに送信するオフチェーンコードを作成する必要があります。これを直接Contractモナドで行う代わりに、cooked-validatorsライブラリーに頼ります。 lockFunds トランザクションは、以下のように作成できます。

lockFunds :: (MonadBlockChain m) => SplitData -> m ()
lockFunds s@SplitData{amount} = void $ validateTxConstr
  [PaysScript splitValidator [(datum, Ada.toValue amount)]]

これは、Contractモナドディレクトリーで作成した lockFundsと酷似しています。 違いは、ここで任意のMonadBlockChainモナドを使用したことです。 この手法により、同じlockFundsを2つの目的に使用することができます。

  1. トランザクションの生成(ContractモナドはMonadBlockChainのインスタンスであるため)

  1. cooked-validatorsの機能を使用したオンチェーンバリデーター用テストの作成

unlockFundsトランザクション(使用するコード)も定義したとしましょう。これにより、cooked-validatorsはContractモナドとシームレスに相互作用します。実際、endpoints関数も チュートリアルにある通りに定義することができます。

endpoints :: (AsContractError e) => Promise w SplitSchema e ()
endpoints = selectList [lock, unlock]
  where
    lock = endpoint @"lock" (lockFunds . mkSplitData)
    unlock = endpoint @"unlock" (const unlockFunds)

コントラクトのテスト

オフチェーンコードの最初のレイヤー(ロートランザクションを生成して送信する)をcooked-validatorsで定義したので、そのテストインフラを使用してオンチェーンバリデーターをテストできます。ロックされた資金を解除できるかどうかの基本的なテストは以下のようになります。

unlockPossible1 = assertSucceeds $ do
  lockFunds sd `as` wallet 1 -- sends the lockFunds pretending to be user 1,
  unlockFunds `as` wallet 2 -- sends the unlockFunds pretending to be user 2.
where
  -- makes a split of 10 ada between users 2 and 3 that only those users should be able to unlock.
  sd = SplitData (wallet 2) (wallet 3) 10

ここで、asコンビネーターはコードのテスト時にのみ機能し、多くのユーザーが行うのと同じ方法でコントラクトとやり取りすることを可能にします。

unlockPossible1関数は良いことが起こるかどうかをチェックするユニットテストです。悪いことが起こらないことをテストすることも簡単にできます。

unlockImpossible1 = assertFails $ do
  lockFunds sd `as` wallet 1
  unlockFunds `as` wallet 5 -- user 5 shouldn't be able to unlock the funds.
where
  sd = SplitData (wallet 2) (wallet 3) 10

これらのテストをプロパティベーステストとして使用することもできます。このケースでは、テストするプロパティは分割の2つの受信者のいずれかが常にロックを解除できることです。

unlockProp1 = forAllTr tr assertSucceeds
  where
    tr = do
      -- generates a random SplitData
      sd <- genSplitData
      -- generates a random wallet; anyone can lock funds.
      w <- genArbitraryWallet
      lockFunds sd `as` w
      -- but only the recipients can unlock the funds
      unlocker <- choose [ recipient1 params , recipient2 params ]
      unlockFunds `as` unlocker

さらに、テストが1つでも失敗した場合、テストの失敗の原因となるトランザクションの読み取り可能な概要を受け取ることができます。これは、より複雑なバリデーターのテスト失敗から抜粋した最初の3つのトランザクションです。

1) ValidateTxSkel
     - Signers: [wallet #1 (a2c20c)]
     - Label: ProposalSkel 2(Payment{paymentAmount = 4200000,paymentRecipient = a96a66})
     - Constraints:
        /\ Mints
            - (18ab4cc $ "threadToken"): 1
            - Policies: [18ab4c]
        /\ PaysScript script 9d52e00:
            - Accumulator{payment = Payment{paymentAmount = 4200000,paymentRecipient = a96a66},signees = []}
              { Lovelace: 6200000
                (18ab4cc $ "threadToken"): 1 }

2) ValidateTxSkel
     - Signers: [wallet #1 (a2c20c)]
     - Constraints:
        /\ PaysScript script 9d52e00:
            - Sign{signPk = a2c20c,signSignature = 8fef22}
              Lovelace: 1

3) ValidateTxSkel
     - Signers: [wallet #2 (80a4f4)]
     - Constraints:
        /\ PaysScript script 9d52e00:
            - Sign{signPk = 80a4f4,signSignature = 6853e0}
              Lovelace: 1
...

開発者に表示されたトレースには問題を修正するために必要なすべての情報が含まれており、読み取り可能な形で情報を提示しようとしています。

プロパティベーステストに加えて、cooked-validatorsは一部の関数に従ってトレース内でトランザクションを修正する機能も提供します。これで、さまざまな攻撃をシミュレーションすることができます。たとえば、以下のようなテストを作成します。

attackNotPossibleOnSplit = assertFails $ do
  somewhere doAttack $ do
    lockFunds sd `as` wallet 1
    unlockFunds `as` wallet 2
 where
  sd = SplitData (wallet 2) (wallet 3) 10

cooked-validatorsは2つのテストを実行しようとしますが、2つとも失敗するはずです。

  1. lockFunds sdをdoAttackに従って修正し、送信、その後修正していないunlockFundsを送信 または

  2. lockFunds sdを送信、その後unlockFundsをdoAttackに従って修正し、送信

somewhereコンビネーターの詳細はやや複雑なため、ここでは省略します。関心がある方は、Tweagブログの別の記事で技術的な詳細を解説していますのでご覧ください。

関連ライブラリーと結論

PlutusはContractModelクラスですでにコントラクトエンドポイントのプロパティベーステストをサポートしていますが、ここではトランザクションレベルのテストは提供されていません。トランザクションレベルのテストはTweagにとって非常に重要です。Plutusコントラクトを監査する際、バリデーターの反応を見るために攻撃者としてふるまい、トランザクションを変更する必要があります。

cooked-validatorsをオフチェーンコードの開発に使用することにより、オンチェーンコードの安全性と正確性に関する多くのプロパティをテストすることができるようになり、コードの正確性に対する確信が高まります。これは、監査時の時間とコストの節約につながります。実際、Tweag監査の第一歩は、 cooked-validatorsを使用したトランザクション生成コードの作成であり、そのあとでクライアントのインフラと自由にやり取りすることが可能になります。