🐡

Solidity基礎学習24日目(Truffleのデバッグ手法 )

に公開

Solidityデバッグ技術 - console.logとtruffle debugによる効率的なデバッグ手法

日付: 2025年9月21日
学習内容: Solidityでのデバッグ技術、console.logパッケージの活用、truffle debugコマンドによる高度なデバッグ手法について

1. Solidityデバッグの概要

1.1 デバッグの重要性

スマートコントラクトのデバッグは、開発プロセスにおいて最も重要な要素の一つです。一度デプロイされたコントラクトは変更できないため、デプロイ前の徹底的なデバッグが必須となります。

デバッグの重要性:

  • 不変性: デプロイ後のコード変更は不可能
  • 資金の安全性: バグによる資金損失の防止
  • ユーザー体験: スムーズな動作の保証
  • セキュリティ: 脆弱性の早期発見

1.2 デバッグ手法の種類

// 1. console.log によるログ出力
console.log("Variable value:", variable);

// 2. truffle debug によるステップ実行
truffle debug <TXHASH>

// 3. require文による条件チェック
require(condition, "Error message");

// 4. イベントによる状態追跡
emit DebugEvent(value, timestamp);

2. @ganache/console.logパッケージの導入

2.1 パッケージのインストール

console.log機能を使用するためには、専用のパッケージをインストールする必要があります。

npm install @ganache/console.log

パッケージの特徴:

  • 軽量: 最小限のオーバーヘッド
  • 互換性: Ganache環境での動作保証
  • 柔軟性: 複数のデータ型に対応
  • 効率性: ガス効率の最適化

2.2 インポートと基本設定

コントラクトでのインポート

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

// console.logパッケージのインポート
import "@ganache/console.log/console.sol";

contract SpaceTiger is ERC721, Ownable {
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("SpaceTiger", "STG")
        Ownable(initialOwner)
    {}

    // デバッグ用のログ出力機能を活用
}

インポートの詳細説明

import "@ganache/console.log/console.sol";

インポートの効果:

  • console.log関数: ログ出力機能の提供
  • 型安全性: 型チェックによる安全性確保
  • ガス最適化: 本番環境での自動削除
  • デバッグ効率: リアルタイムでの値確認

3. console.logによるデバッグ実装

3.1 基本的なログ出力

単純な値の出力

function buyToken() public payable {
    uint256 tokenId = _nextTokenId;
    
    // 基本的なログ出力
    console.log("Function called", tokenId);
    console.log("msg.value", msg.value);
    
    require(msg.value == (tokenId + 1) * 0.1 ether, "Not enough funds sent");
    
    _nextTokenId++;
    _safeMint(msg.sender, tokenId);
}

複数の値の同時出力

function buyToken() public payable {
    uint256 tokenId = _nextTokenId;
    
    // 複数の値を同時に出力
    console.log("got here", tokenId, msg.value);
    
    uint256 requiredAmount = (tokenId + 1) * 0.1 ether;
    console.log("Required amount:", requiredAmount);
    console.log("Current value:", msg.value);
    
    require(msg.value == requiredAmount, "Not enough funds sent");
    
    _nextTokenId++;
    _safeMint(msg.sender, tokenId);
}

3.2 高度なデバッグパターン

条件分岐でのデバッグ

function buyToken() public payable {
    uint256 tokenId = _nextTokenId;
    
    console.log("=== buyToken Debug Start ===");
    console.log("Current tokenId:", tokenId);
    console.log("msg.sender:", msg.sender);
    console.log("msg.value:", msg.value);
    
    uint256 requiredAmount = (tokenId + 1) * 0.1 ether;
    console.log("Required amount:", requiredAmount);
    
    if (msg.value < requiredAmount) {
        console.log("ERROR: Insufficient funds");
        console.log("Shortage:", requiredAmount - msg.value);
        revert("Not enough funds sent");
    }
    
    console.log("SUCCESS: Payment verified");
    
    _nextTokenId++;
    _safeMint(msg.sender, tokenId);
    
    console.log("Token minted successfully, new tokenId:", tokenId);
    console.log("=== buyToken Debug End ===");
}

ループ処理でのデバッグ

function batchMint(address[] memory recipients) public onlyOwner {
    console.log("=== Batch Mint Debug Start ===");
    console.log("Number of recipients:", recipients.length);
    
    for (uint256 i = 0; i < recipients.length; i++) {
        console.log("Processing recipient", i, recipients[i]);
        
        uint256 tokenId = _nextTokenId++;
        _safeMint(recipients[i], tokenId);
        
        console.log("Minted tokenId:", tokenId, "to:", recipients[i]);
    }
    
    console.log("=== Batch Mint Debug End ===");
}

3.3 データ型別の出力方法

基本的なデータ型

function debugDataTypes() public {
    // 整数型
    uint256 number = 123;
    int256 signedNumber = -456;
    console.log("uint256:", number);
    console.log("int256:", signedNumber);
    
    // アドレス型
    address user = msg.sender;
    console.log("address:", user);
    
    // ブール型
    bool isTrue = true;
    console.log("bool:", isTrue);
    
    // 文字列型
    string memory message = "Hello World";
    console.log("string:", message);
}

複合データ型

function debugComplexTypes() public {
    // 配列
    uint256[] memory numbers = new uint256[](3);
    numbers[0] = 1;
    numbers[1] = 2;
    numbers[2] = 3;
    
    console.log("Array length:", numbers.length);
    for (uint256 i = 0; i < numbers.length; i++) {
        console.log("Array[", i, "]:", numbers[i]);
    }
    
    // 構造体
    struct User {
        address userAddress;
        uint256 tokenCount;
        bool isActive;
    }
    
    User memory user = User({
        userAddress: msg.sender,
        tokenCount: 5,
        isActive: true
    });
    
    console.log("User address:", user.userAddress);
    console.log("User tokenCount:", user.tokenCount);
    console.log("User isActive:", user.isActive);
}

4. truffle debugコマンドによる高度なデバッグ

4.1 truffle debugの基本概念

デバッグコマンドの実行

# 基本的なデバッグコマンド
truffle debug <TXHASH>

# ネットワーク指定でのデバッグ
truffle debug <TXHASH> --network sepolia

# デバッグセッションの開始
truffle debug

truffle debugの特徴:

  • ステップ実行: 命令レベルでの実行制御
  • 変数監視: 実行時の変数値確認
  • スタック追跡: 呼び出しスタックの可視化
  • ガス監視: ガス消費量の詳細分析

4.2 デバッグセッションの操作

基本的なデバッグコマンド

# デバッグセッション内でのコマンド
(debug)> help                    # ヘルプの表示
(debug)> o                       # 次の命令に進む
(debug)> i                       # 関数内部に入る
(debug)> u                       # 関数から出る
(debug)> n                       # 次の行に進む
(debug)> s                       # ステップ実行
(debug)> c                       # 実行を続行
(debug)> q                       # デバッグを終了

変数とスタックの監視

# 変数の監視
(debug)> v                       # ローカル変数の表示
(debug)> l                       # 現在の行の表示
(debug)> b <line_number>         # ブレークポイントの設定
(debug)> watch <variable_name>   # 変数の監視設定

# スタック情報の確認
(debug)> t                       # スタックトレースの表示
(debug)> s                       # スタックの詳細表示

4.3 実践的なデバッグ例

エラートランザクションのデバッグ

# 1. 失敗したトランザクションのハッシュを取得
truffle migrate --network ganache

# 出力例:
# Deploying 'SpaceTiger'
# transaction hash:    0x1234567890abcdef...
# gas used:            1234567
# block number:        123
# block timestamp:     1640995200
# account:             0xabcdef1234567890...
# balance:             99.87654321
# gas price:           20000000000 wei
# gas limit:           5000000
# value sent:          0 ETH
# total cost:          0.02469134 ETH

# 2. デバッグの開始
truffle debug 0x1234567890abcdef...

# 3. デバッグセッションでの操作
(debug)> o                       # 次の命令に進む
(debug)> v                       # 変数を確認
(debug)> l                       # 現在の行を確認
(debug)> s                       # ステップ実行

複雑なロジックのデバッグ

function complexFunction(uint256 amount, address recipient) public {
    console.log("=== Complex Function Debug ===");
    console.log("Input amount:", amount);
    console.log("Input recipient:", recipient);
    
    // 条件チェック1
    require(amount > 0, "Amount must be positive");
    console.log("PASS: Amount validation");
    
    // 条件チェック2
    require(recipient != address(0), "Invalid recipient");
    console.log("PASS: Recipient validation");
    
    // 計算処理
    uint256 fee = amount * 5 / 100; // 5% fee
    uint256 netAmount = amount - fee;
    
    console.log("Calculated fee:", fee);
    console.log("Net amount:", netAmount);
    
    // バランスチェック
    require(address(this).balance >= amount, "Insufficient contract balance");
    console.log("PASS: Balance validation");
    
    // 実行処理
    payable(recipient).transfer(netAmount);
    console.log("Transfer completed");
    
    console.log("=== Complex Function Debug End ===");
}

4.4 デバッグ戦略とベストプラクティス

段階的デバッグアプローチ

function debugStrategy() public {
    // レベル1: 基本的な値の確認
    console.log("Level 1: Basic values");
    console.log("msg.sender:", msg.sender);
    console.log("msg.value:", msg.value);
    console.log("block.timestamp:", block.timestamp);
    
    // レベル2: 計算結果の確認
    console.log("Level 2: Calculations");
    uint256 result1 = calculateValue1();
    console.log("Result 1:", result1);
    
    uint256 result2 = calculateValue2();
    console.log("Result 2:", result2);
    
    // レベル3: 条件分岐の確認
    console.log("Level 3: Conditions");
    if (result1 > result2) {
        console.log("Condition: result1 > result2");
        executeBranchA();
    } else {
        console.log("Condition: result1 <= result2");
        executeBranchB();
    }
    
    // レベル4: 最終結果の確認
    console.log("Level 4: Final result");
    console.log("Final state updated");
}

パフォーマンス考慮事項

contract DebugOptimized {
    // 本番環境での自動削除
    bool private constant DEBUG_MODE = false;
    
    function debugLog(string memory message, uint256 value) internal {
        if (DEBUG_MODE) {
            console.log(message, value);
        }
    }
    
    function productionFunction() public {
        debugLog("Debug info", 123);  // 本番環境では削除される
        
        // 本番ロジック
        performMainLogic();
    }
}

5. デバッグ環境の設定と最適化

5.1 開発環境の構築

Ganacheでのデバッグ環境

# Ganacheの起動(デバッグ用設定)
npx ganache --host 0.0.0.0 --port 8545 --networkId 5777 --gasLimit 6721975 --gasPrice 20000000000 --accounts 10 --deterministic --db ./ganache_db

# デバッグ用の追加オプション
npx ganache --verbose --debug

truffle-config.jsの最適化

module.exports = {
  networks: {
    ganache_debug: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*",
      gas: 6721975,
      gasPrice: 20000000000,
      // デバッグ用の設定
      skipDryRun: true,
      confirmations: 0,
      timeoutBlocks: 1,
      networkCheckTimeout: 10000
    }
  },
  
  compilers: {
    solc: {
      version: "0.8.24",
      settings: {
        optimizer: {
          enabled: false,  // デバッグ時は最適化を無効化
          runs: 200
        },
        evmVersion: "shanghai",
        debug: {
          revertStrings: "debug"  // デバッグ情報の保持
        }
      }
    }
  },
  
  mocha: {
    timeout: 100000,
    // デバッグ用の詳細出力
    reporter: 'spec',
    reporterOptions: {
      verbose: true
    }
  }
};

5.2 ログ管理とフィルタリング

構造化されたログ出力

contract StructuredDebug {
    event DebugLog(string category, string message, uint256 value);
    event ErrorLog(string function, string message, uint256 timestamp);
    
    modifier debugOnly() {
        require(block.chainid == 1337, "Debug only on testnet"); // Ganache chainid
        _;
    }
    
    function debugFunction() public debugOnly {
        emit DebugLog("FUNCTION_START", "debugFunction called", block.timestamp);
        
        try this.internalFunction() {
            emit DebugLog("FUNCTION_SUCCESS", "internalFunction completed", block.timestamp);
        } catch Error(string memory reason) {
            emit ErrorLog("debugFunction", reason, block.timestamp);
        }
        
        emit DebugLog("FUNCTION_END", "debugFunction completed", block.timestamp);
    }
}

ログフィルタリング戦略

# 特定のログのみをフィルタリング
truffle logs --filter "DebugLog" --network ganache

# エラーログのみを表示
truffle logs --filter "ErrorLog" --network ganache

# 時間範囲でのフィルタリング
truffle logs --from-block 100 --to-block 200 --network ganache

6. 高度なデバッグテクニック

6.1 ガスデバッグと最適化

ガス消費の詳細分析

function gasDebugFunction() public {
    uint256 gasStart = gasleft();
    console.log("Gas at start:", gasStart);
    
    // 処理1
    uint256 value1 = calculateExpensiveOperation();
    uint256 gasAfterOp1 = gasleft();
    console.log("Gas after operation 1:", gasAfterOp1);
    console.log("Gas consumed by op1:", gasStart - gasAfterOp1);
    
    // 処理2
    uint256 value2 = calculateAnotherOperation();
    uint256 gasAfterOp2 = gasleft();
    console.log("Gas after operation 2:", gasAfterOp2);
    console.log("Gas consumed by op2:", gasAfterOp1 - gasAfterOp2);
    
    // 最終結果
    uint256 totalGasConsumed = gasStart - gasleft();
    console.log("Total gas consumed:", totalGasConsumed);
}

メモリ使用量の監視

function memoryDebugFunction() public {
    console.log("=== Memory Debug Start ===");
    
    // メモリ使用量の測定
    assembly {
        let free_mem := mload(0x40)
        console.log("Free memory pointer:", free_mem);
    }
    
    // 大きなデータ構造の作成
    uint256[] memory largeArray = new uint256[](1000);
    for (uint256 i = 0; i < 1000; i++) {
        largeArray[i] = i;
    }
    
    assembly {
        let free_mem_after := mload(0x40)
        console.log("Free memory after array:", free_mem_after);
    }
    
    console.log("=== Memory Debug End ===");
}

6.2 状態遷移の追跡

コントラクト状態の監視

contract StateTracking {
    struct StateSnapshot {
        uint256 blockNumber;
        uint256 timestamp;
        mapping(string => uint256) values;
    }
    
    StateSnapshot[] public stateHistory;
    
    event StateChanged(string key, uint256 oldValue, uint256 newValue);
    
    function updateState(string memory key, uint256 newValue) public {
        // 前の状態を記録
        uint256 oldValue = getCurrentValue(key);
        
        // 新しい状態を設定
        setCurrentValue(key, newValue);
        
        // 状態変更をログ出力
        console.log("State change:", key, "from", oldValue, "to", newValue);
        emit StateChanged(key, oldValue, newValue);
        
        // 状態スナップショットを作成
        createStateSnapshot();
    }
    
    function createStateSnapshot() internal {
        // 現在の状態をスナップショットとして保存
        console.log("Creating state snapshot at block:", block.number);
    }
}

6.3 外部コントラクトとの連携デバッグ

外部コントラクト呼び出しのデバッグ

contract ExternalContractDebug {
    IERC20 public token;
    
    function debugExternalCall(address tokenAddress, uint256 amount) public {
        console.log("=== External Call Debug ===");
        console.log("Token address:", tokenAddress);
        console.log("Amount to transfer:", amount);
        
        // コントラクトの状態確認
        console.log("Contract balance before:", token.balanceOf(address(this)));
        console.log("User balance before:", token.balanceOf(msg.sender));
        
        // 外部コントラクト呼び出し
        try token.transferFrom(msg.sender, address(this), amount) {
            console.log("SUCCESS: Transfer completed");
            console.log("Contract balance after:", token.balanceOf(address(this)));
            console.log("User balance after:", token.balanceOf(msg.sender));
        } catch Error(string memory reason) {
            console.log("ERROR: Transfer failed -", reason);
        } catch (bytes memory lowLevelData) {
            console.log("ERROR: Low level error occurred");
            console.log("Error data length:", lowLevelData.length);
        }
        
        console.log("=== External Call Debug End ===");
    }
}

7. デバッグツールの統合と自動化

7.1 自動テストとの連携

デバッグ情報付きテスト

// test/debug.test.js
const SpaceTiger = artifacts.require("SpaceTiger");

contract("SpaceTiger Debug Tests", (accounts) => {
    let spaceTigerInstance;
    
    beforeEach(async () => {
        spaceTigerInstance = await SpaceTiger.deployed();
    });
    
    it('should debug buyToken function', async () => {
        const initialTokenId = await spaceTigerInstance._nextTokenId();
        console.log("Initial tokenId:", initialTokenId.toString());
        
        // デバッグ情報を確認しながらトランザクションを実行
        const tx = await spaceTigerInstance.buyToken({
            value: web3.utils.toWei("0.1", "ether"),
            from: accounts[1]
        });
        
        console.log("Transaction hash:", tx.tx);
        console.log("Gas used:", tx.receipt.gasUsed);
        
        // デバッグログの確認
        const logs = tx.logs;
        console.log("Number of logs:", logs.length);
        
        for (let i = 0; i < logs.length; i++) {
            console.log(`Log ${i}:`, logs[i]);
        }
    });
    
    it('should debug failed transaction', async () => {
        try {
            // 意図的に失敗させるトランザクション
            await spaceTigerInstance.buyToken({
                value: web3.utils.toWei("0.05", "ether"), // 不足額
                from: accounts[1]
            });
            assert.fail("Transaction should have failed");
        } catch (error) {
            console.log("Expected error caught:", error.message);
            // エラーの詳細分析
            console.log("Error type:", error.constructor.name);
        }
    });
});

7.2 継続的インテグレーションでのデバッグ

CI/CDパイプラインでのデバッグ

# .github/workflows/debug.yml
name: Debug and Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  debug-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '18'
        
    - name: Install dependencies
      run: |
        npm install
        npm install @ganache/console.log
        
    - name: Start Ganache
      run: |
        npx ganache --host 0.0.0.0 --port 8545 --deterministic &
        sleep 5
        
    - name: Compile contracts
      run: npx truffle compile
      
    - name: Run tests with debug
      run: |
        npx truffle test --network ganache
        npx truffle migrate --network ganache
        
    - name: Debug specific transaction
      run: |
        # 特定のトランザクションをデバッグ
        TXHASH=$(npx truffle migrate --network ganache | grep "transaction hash" | tail -1 | cut -d' ' -f3)
        echo "Debugging transaction: $TXHASH"
        # デバッグコマンドの実行(自動化)

8. パフォーマンスとセキュリティの考慮事項

8.1 本番環境でのデバッグコード管理

条件付きデバッグの実装

contract ProductionReady {
    // 本番環境でのデバッグコードの自動削除
    bool private constant DEBUG_ENABLED = false;
    
    modifier debugOnly() {
        require(DEBUG_ENABLED || block.chainid == 1337, "Debug disabled in production");
        _;
    }
    
    function debugFunction() public debugOnly {
        console.log("This will only run in debug mode");
        // デバッグロジック
    }
    
    function productionFunction() public {
        // 本番ロジック(デバッグコードなし)
        performMainLogic();
    }
}

ガス効率の最適化

contract GasOptimized {
    // デバッグ情報の効率的な管理
    struct DebugInfo {
        bool enabled;
        uint256 lastLogBlock;
        uint256 logCount;
    }
    
    DebugInfo private debugInfo;
    
    function logDebug(string memory message, uint256 value) internal {
        if (debugInfo.enabled && block.number > debugInfo.lastLogBlock) {
            console.log(message, value);
            debugInfo.logCount++;
            debugInfo.lastLogBlock = block.number;
        }
    }
    
    function optimizedFunction() public {
        logDebug("Function called", block.timestamp);
        
        // メインロジック
        performLogic();
        
        logDebug("Function completed", gasleft());
    }
}

8.2 セキュリティ考慮事項

機密情報の保護

contract SecureDebug {
    // 機密情報のマスキング
    function debugWithMasking(address user, uint256 privateValue) public {
        // アドレスの一部のみを表示
        console.log("User (masked):", maskAddress(user));
        
        // 機密値のハッシュ化
        bytes32 hashedValue = keccak256(abi.encodePacked(privateValue, block.timestamp));
        console.log("Private value hash:", hashedValue);
        
        // デバッグ情報の制限
        require(msg.sender == owner(), "Only owner can debug");
    }
    
    function maskAddress(address addr) internal pure returns (string memory) {
        bytes20 addrBytes = bytes20(addr);
        return string(abi.encodePacked(
            "0x",
            toHexString(addrBytes[0]),
            toHexString(addrBytes[1]),
            "****",
            toHexString(addrBytes[18]),
            toHexString(addrBytes[19])
        ));
    }
}

まとめ

Solidityでのデバッグ技術は、スマートコントラクト開発において不可欠な要素です。@ganache/console.logパッケージとtruffle debugコマンドを組み合わせることで、効率的で包括的なデバッグ環境を構築できます。

主要な学習ポイント:

  1. console.logの活用: リアルタイムでの変数値確認とログ出力
  2. truffle debugの活用: ステップ実行による詳細なデバッグ
  3. 構造化されたデバッグ: 段階的なアプローチとログ管理
  4. パフォーマンス考慮: 本番環境での最適化とガス効率
  5. セキュリティ配慮: 機密情報の保護とアクセス制御

これらの技術を適切に活用することで、より安全で信頼性の高いスマートコントラクトの開発が可能になります。

参考:

Discussion