Fracton テックブログ

FractonのEthereum周辺技術について発信しています

Foundryを使ったDifferential testingについて

こんにちは、Fractonのリサーチエンジニアの池田です。

今回はFoundryのDifferential testingについて説明します。

こちらが公式のDocsです。

book.getfoundry.sh

背景

Differential testingとは、同様のアルゴリズムや関数に関する複数の異なる実装の実行結果が一致していることを確認するテスト手法です。FoundryではSolidityで実装された関数の出力を、PythonJavaScriptで書いた別の実装の出力と一致しているか確認することができます。

たとえば複雑な数式を使うF(x)という関数をSolidityで実装したいとします。このときSolidityでF(x)を実装したものをf1(x)、PythonでF(x)を実装したものをf2(x)とします。さまざまなxに対し、f1(x) == f2(x)を確認することで、Solidityでの計算結果を外部の計算結果と突き合わせ検算することができます。このようにさまざまなリソースを参照することで複雑な数式の検証も効率的に行うことができます。

FFIについて

Differential testingを実行するため、 FFI*1機能を使います。 FoundryではFFIを使うことで、テストコードで任意のシェルコマンドを実行できます。

例えば以下のテストのようにcmdsを定義すると、cats address.txtというシェルコマンドが、vm.ffi(cmds)で実行され、resultにaddress.txtの中身が代入されます。

import "forge-std/Test.sol";

contract TestContract is Test {

    function testMyFFI () public {
        string[] memory cmds = new string[](2);
        cmds[0] = "cat";
        cmds[1] = "address.txt"; // assume contains abi-encoded address.
        bytes memory result = vm.ffi(cmds);
        address loadedAddress = abi.decode(result, (address));
        // Do something with the address
        // ...
    }
}

上記の例では、abi.encodeされたアドレスをaddress.txtに書いておけば、FFIで読み込んだあとabi.decodeすることでloadedAddressとして使うことができます。

Gradual Dutch Auctionの例

公式DocsでDifferential testingのサンプルとしてGradual Dutch Auctionが紹介されています。

github.com

この例では、価格の数学的モデルとして、以下の式をコントラクトで実装しています。 Paradigm参考文献

このような複雑な数式のテストを手作業で書くのは非常に大変ですし、思いがけないバグがどこかにありそれに気付けない可能性があります。そんな時に、Differential testingを使うことにより外部ソースとの検証を効率的に行うことができます。

以下ソースコードを見てみます。 src/test/DiscreteGDA.t.solに、ffiを使ったDifferential testingが使われています。inputsが実行されるシェルコマンドで、analysis/compute_price.pyというPythonスクリプトを実行し、外部から計算結果を呼び出しています。

//call out to python script for price computation
function calculatePrice(
    int256 _initialPrice,
    int256 _scaleFactor,
    int256 _decayConstant,
    uint256 _numTotalPurchases,
    uint256 _timeSinceStart,
    uint256 _quantity
) private returns (uint256) {
    string[] memory inputs = new string[](15);
    inputs[0] = "python3";
    inputs[1] = "analysis/compute_price.py";
    inputs[2] = "exp_discrete";
    inputs[3] = "--initial_price";
    inputs[4] = uint256(_initialPrice).toString();
    inputs[5] = "--scale_factor";
    inputs[6] = uint256(_scaleFactor).toString();
    inputs[7] = "--decay_constant";
    inputs[8] = uint256(_decayConstant).toString();
    inputs[9] = "--num_total_purchases";
    inputs[10] = _numTotalPurchases.toString();
    inputs[11] = "--time_since_start";
    inputs[12] = _timeSinceStart.toString();
    inputs[13] = "--quantity";
    inputs[14] = _quantity.toString();
    bytes memory res = vm.ffi(inputs);
    uint256 price = abi.decode(res, (uint256));
    return price;
}

最終的にはcheckPriceWithParametersという部分で、Solidityの結果と、Pythonの結果が0.1%以内の精度で一致していることを確認しています。

//equal within 0.1%
utils.assertApproxEqual(actualPrice, expectedPrice, 1);

このように複雑な数式のExpected Valueとして、外部スクリプトの結果を利用することができ非常に便利です。また、fuzz testingを組み合わせることにより、さまざまな入力xについてf1(x) == f2(x)をテストすることができ、テストを効率的に行うことができます。

まとめ

Differentialテストを用いることで、手動ではテストを書くのが難しい複雑なアルゴリズムや関数も効率的にテストすることができる。

*1:おそらくForeign function interfaceの略。あるプログラミング言語から別の言語で書かれた関数などを利用する機能のことです。