🐡
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コマンドを組み合わせることで、効率的で包括的なデバッグ環境を構築できます。
主要な学習ポイント:
- console.logの活用: リアルタイムでの変数値確認とログ出力
- truffle debugの活用: ステップ実行による詳細なデバッグ
- 構造化されたデバッグ: 段階的なアプローチとログ管理
- パフォーマンス考慮: 本番環境での最適化とガス効率
- セキュリティ配慮: 機密情報の保護とアクセス制御
これらの技術を適切に活用することで、より安全で信頼性の高いスマートコントラクトの開発が可能になります。
参考:
Discussion