foundry

foundryのDifferentialTestについて

foundryのDifferentialTestについて

foundryのDifferentialTestは、foundryのforge機能の一つで、nodejsやpythonなどの他の言語のプログラムの実行をsolidityのテストから実行することができます。

そのため、他の言語のプログラムの実行結果とsolidityのテストの実行結果を比較することで、solidityのプログラムが正常に働いているかチェックできます。

ここでは、実際に簡易的なテストを紹介しながらDifferentialTestの作り方を紹介していきたいと思います。

下準備

ここで使用するテストコードなどは全て下記に保存しています。

https://github.com/eggdragons/how-to-use-foundry

まずは、nodejs関連のパッケージをインストールします。

nodejs関連のプログラムは、ts-srcフォルダに保存しています。

ルートフォルダ下記コマンドを実行する事でサブフォルダ側のインストールも自動で実行されるようにしてあるので、まずは下のコマンドを入力してください。

npm install

今回実際に行なっていくテストは、testフォルダにあるSimpleDifferentialTestになります。

シンプルなDifferentialTest(testContractVsTsAdd)

コントラクトの計算結果とnodejsの計算結果を比較していきます。

計算内容は、至ってシンプルな足し算を行います。

対象のコントラクトは、src/Calculation.solで、計算内容は下記の通り。

function contractFunctionAdd(uint256 a, uint256 b) public pure returns (uint256) {
    return a + b;
}

一方で、対象のnodejsは、ts-src/add.tsで、計算内容は下記の通り。

const c = a + b;

テストの内容がこちら

    function testContractVsTsAdd() public {
        uint256 a = 1;
        uint256 b = 2;

        string[] memory runTsInputs = new string[](5);
        runTsInputs[0] = "npx";
        runTsInputs[1] = "ts-node";
        runTsInputs[2] = "ts-src/add.ts";
        runTsInputs[3] = Strings.toString(a);
        runTsInputs[4] = Strings.toString(b);

        bytes memory tsResult = vm.ffi(runTsInputs);
        uint256 tsAdd = abi.decode(tsResult, (uint256));

        uint256 add = contractFunctionAdd(a, b);

        assertEq(tsAdd, add);
    }

上から順に見ていくと、aとbにそれぞれ値を与えています。

runTsInputsにて、起動したいコマンドを格納しています。格納の仕方としてはスペースごとに格納しています。

runTsInputs[3]やrunTsInputs[4]は、nodejsに渡したい引数を格納しています。

nodejs側は、process.argv[2]で引数を取得していきます。この辺りは、nodejsの通常の使い方と同じですね

import { ethers } from "ethers";

const a = Number(process.argv[2]);
const b = Number(process.argv[3]);

const c = a + b;

console.log(ethers.utils.defaultAbiCoder.encode(["uint256"], [c]));

nodejs側で出力した(console.log)値が、返却されます。

出力値や戻り値については、状況に合わせて変換して対応します。

実際にテストを行ってみましょう

forge test --ffi --match-contract DifferentialTests --match-test testContractVsTsAdd -vvvvv   

すると、問題なくテスト合格すると思います。

Fuzz機能を追加したDifferentialTest(testFuzzContractVsTsAdd)

次にFuzz機能を追加して再度同じようなテストを行います。

このテストは非常に時間がかかります。私のパソコンだと大体250sくらいかかります。

    function testFuzzTestContractVsTsAdd(uint8 a, uint8 b) public {
        vm.assume(a > 0 && b > 0);

        string[] memory runTsInputs = new string[](5);
        runTsInputs[0] = "npx";
        runTsInputs[1] = "ts-node";
        runTsInputs[2] = "ts-src/add.ts";
        runTsInputs[3] = Strings.toString(a);
        runTsInputs[4] = Strings.toString(b);

        bytes memory tsResult = vm.ffi(runTsInputs);
        uint256 tsAdd = abi.decode(tsResult, (uint256));

        uint256 add = a + b;

        assertEq(tsAdd, add);
    }

Fuzzing機能を使うために、1行目の引数にuint8 a, uint8 bを追記して、代入式を削除しています。

また、今回は、コントラクトでの計算ではなく、テストに計算式を直接書いています。(14行目uint256 add = a + b;

実際にテストを行ってみましょう

forge test --ffi --match-contract DifferentialTests --match-test testFuzzTestContractVsTsAdd -vvvvv

すると、Arithmetic over/underflowのエラーが発生すると思います。

そこで、今度は、この足し算の部分をuint256 add = SafeMath.add(a, b);に変更して再計算したいと思います。

forge test --ffi --match-contract DifferentialTests --match-test testFuzzTestSafeMathVsTsAdd -vvvvv

すると、今度は、問題なくテスト合格すると思います。

適切な言葉がわからないのですが、型ガードと言うんですかね?それが働いていることがわかったかと思います。

事前コンパイルしてスピードアップDifferentialTest(testFuzzContractVsJsAdd)

さっきのテストは非常に時間がかかりましたよね。

これは、TypeScriptをJavaScriptにコンパイルしているため非常に時間がかかっています。

そこで、次のテストでは、事前にTypeScriptをJavaScriptにコンパイルしてからテストを実行してみましょう。

次のコマンドを実行することで、TypeScriptがJavaScriptにコンパイルされます。

npm run compile

ts-srcフォルダにJsファイルが保存されていると思いますので、テストを実行してみましょう!

forge test --ffi --match-contract DifferentialTests --match-test testFuzzContractVsJsAdd -vvvvv    

先ほどのテストよりも早く結果が出たかと思います。私のパソコンだと大体30sくらいです。

複数の計算を行うDifferentialTest(testFuzzContractVsJsAddAndSub)

次は、複数の計算を行ってみましょう!

これもたいして難しい事はなくて、nodejsから出力する時に一つにまとめて出力し、テスト側でdecodeするだけになります!

対象のnodejsは、ts-src/addAndSub.tsです。

import { ethers } from "ethers";

const a = Number(process.argv[2]);
const b = Number(process.argv[3]);

const add = a + b;
const sub = a - b;

console.log(
  ethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [add, sub])
);

10行目のethers.utils.defaultAbiCoder.encode(["uint256", "uint256"], [add, sub])を見てもらうとわかるかと思います。

テストの内容がこちら


    function testFuzzContractVsJsAddAndSub(uint8 a, uint8 b) public {
        vm.assume(a > 0 && b > 0 && a > b);

        string[] memory runJsInputs = new string[](4);
        runJsInputs[0] = "node";
        runJsInputs[1] = "ts-src/addAndSub.js";
        runJsInputs[2] = Strings.toString(a);
        runJsInputs[3] = Strings.toString(b);
        bytes memory tsResult = vm.ffi(runJsInputs);
        (uint256 JsAdd, uint256 JsSub) = abi.decode(tsResult, (uint256, uint256));

        uint256 add = contractFunctionAdd(a, b);
        uint256 sub = contractFunctionSub(a, b);

        assertEq(add, JsAdd);
        assertEq(sub, JsSub);
    }

11行目の(uint256 JsAdd, uint256 JsSub) = abi.decode(tsResult, (uint256, uint256));を見てもらうとわかるかと思います。

テストを実行してみましょう!

forge test --ffi --match-contract DifferentialTests --match-test testFuzzContractVsJsAddAndSub -vvvvv

すると、問題なくテスト合格すると思います。

外部からデータを入力して行うDifferentialTest(testImportDataContractVsJsAddAndSub)

最後に、外部からデータをインポートして、テストを実行してみましょう

まずは、外部からインポートするデータを作成します。

対象のnodejsは、ts-src/createData.tsです。

import * as fs from "fs";
import { ethers } from "ethers";

const a = 10;
const b = 8;

const encodedData = ethers.utils.defaultAbiCoder.encode(
  ["uint8", "uint8"],
  [a, b]
);

const outputDirectory = "../data/";
if (!fs.existsSync(outputDirectory)) {
  fs.mkdirSync(outputDirectory);
}
fs.writeFileSync(outputDirectory + "data.txt", encodedData);

このプログラムは見てもらった通り、10と8をまとめてエンコードしてdata.txtに保存しているだけですね。

早速下記コマンドを実行してみましょう!

npm run create

すると、ルートフォルダにdata/data.txtが作成されたかと思います。

0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000008

中身を見てみると、確かに10と8が記載されてますね!

テストの内容がこちら

    function testImportDataContractVsJsAddAndSub() public {
        string[] memory loadJsDataInputs = new string[](2);
        loadJsDataInputs[0] = "cat";
        loadJsDataInputs[1] = "data/data.txt";
        bytes memory loadResult = vm.ffi(loadJsDataInputs);
        (uint8 a, uint8 b) = abi.decode(loadResult, (uint8, uint8));

        string[] memory runJsInputs = new string[](4);
        runJsInputs[0] = "node";
        runJsInputs[1] = "ts-src/addAndSub.js";
        runJsInputs[2] = Strings.toString(a);
        runJsInputs[3] = Strings.toString(b);
        bytes memory tsResult = vm.ffi(runJsInputs);
        (uint256 JsAdd, uint256 JsSub) = abi.decode(tsResult, (uint256, uint256));

        uint256 add = SafeMath.add(a, b);
        uint256 sub = SafeMath.sub(a, b);

        assertEq(add, JsAdd);
        assertEq(sub, JsSub);
    }

2行目から6行目を見てもらうとわかる通り、このようにaとbの変数を取り込んでいます。

テストを実行してみましょう!

forge test --ffi --match-contract DifferentialTests --match-test testImportDataContractVsJsAddAndSub -vvvvv

すると、問題なくテスト合格すると思います。

この機能を使えば、コントラクト上で正常に動くか確認することができます。Fuzzテストとも組み合わせることでより一層正確なテストができます。既存のJsやpythonなどで作られたプログラムを移行する際に非常に役に立ちますよね〜!

-foundry