3MIKAN
仮想通貨直コン

Ethereumのハッシュが合わないときは?Keccak-256・SHA3-256・文字列とバイト列

同じつもりの入力でハッシュが違う原因を、アルゴリズム、16進数の解釈、改行、Unicode正規化、関数シグネチャへ分け、アカウントなしで検算します。

3MIKANのキャラクターが二つのタイルと六つの粒を拡大鏡で見比べ、入力データの違いを確かめる場面

ハッシュ値がコントラクト側と一致しない。そんなとき、いきなりネットワークやライブラリーの不具合を疑う前に、同じ計算方法へ、同じバイト列を渡しているかを確認します。

この記事は、関数の識別子やイベントのトピックを計算するときに値が合わず困っている開発者向けです。Keccak-256とSHA3-256の違い、16進表記と文字列、末尾の改行を順に切り分けます。外部RPC、ウォレット、APIキーは使いません。

掲載例はNode.js 22以降で実行できます。hash-inputs.mjsを同じフォルダーへ保存し、各節のJavaScriptを別のexample.mjsへ写してnode example.mjsを実行してください。

32バイトの出力でも、同じハッシュ関数とは限らない

EthereumのABIで関数の識別子やイベントの識別に使うのはKeccak-256です。一方、SHA3-256は別の計算です。OpenSSLもKECCAK-256SHA3-256を別の名前で提供しており、片方をもう片方の代用品にはできません。1 2 3

違いを確かめる最小の入力は、空のバイト列です。入力の文字コードやABIの組み立てを考える必要がないため、最初の動作確認に向いています。

空入力のKeccak-256
0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470

空入力のSHA3-256
0xa7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

上の値は、今回のローカル環境で計算して照合しました。どちらも出力の長さは32バイトですが、内容は一致しません。長さだけでアルゴリズムを判定しないことが大切です。

まず実行環境の対応を確認する

原稿の検算はNode.js v22.16.0とOpenSSL CLI 3.5.5で行い、公開準備ではNode.js v24.14.0とOpenSSL CLI 3.6.2でも同じ結果を確認しました。これは実行した環境であり、最新版の案内ではありません。

command -v openssl
openssl version
openssl list -digest-algorithms
printf '%s' '' | openssl dgst -keccak-256
printf '%s' '' | openssl dgst -sha3-256

KECCAK-256が使えることを確認してから進みます。Macではopensslという名前でもLibreSSLが選ばれる場合があり、今回のMac標準のLibreSSL 3.3.6ではこの計算を実行できませんでした。対応するOpenSSLの実行ファイルがあるフォルダーをPATHの先頭へ追加し、上の確認をやり直します。

たとえばHomebrewのopenssl@3を導入済みなら、同じ端末でexport PATH="$(brew --prefix openssl@3)/bin:$PATH"を実行して選択できます。導入方法や場所は環境で異なるため、バージョン表示と空入力の結果で確認してください。hash-inputs.mjsPATHから選ばれるコマンドを使います。エラーを消すためにsha3-256へ置き換えてはいけません。2 3

なお、OSのopensslコマンドと、Node.jsが使うOpenSSLのバージョンは同じとは限りません。今回もNode.js側は3.0.16、CLI側は3.5.5でした。Node.jsの利用可能なハッシュ名はgetHashes()で調べられます。4

import { getHashes } from 'node:crypto';

console.log(process.version);
console.log(process.versions.openssl);
console.log(getHashes().filter((name) => /keccak|sha3/i.test(name)));

「0x1234」は2バイトにも6バイトにもなる

同じ画面に0x1234と表示されていても、関数へ渡すものは二通り考えられます。

入力としての意味 実際のバイト列 長さ
16進数を読み取ったDATA 12 34 2バイト
文字列そのもののUTF-8 30 78 31 32 33 34 6バイト

後者には、文字の0xも含まれます。ハッシュ関数が間違っていなくても、ここが違えば結果は合いません。

次のコードは付属のhash-inputs.mjsと同じフォルダーで実行できます。fromHexは偶数桁の16進表記を検証してバイト列へ変換し、utf8は文字列をUTF-8へ変換します。どちらも不正な入力を黙って補正しません。

import { fromHex, utf8, keccak256 } from './hash-inputs.mjs';

const bytes = fromHex('0x1234');
const text = utf8('0x1234');

console.log(bytes.length, text.length); // 2 6
console.log(keccak256(bytes));
console.log(keccak256(text));
2バイトをハッシュ
0x56570de287d73cd1cb6092bb8fdee6173974955fdef345ae579ee9f475ea7432

6バイトの文字列をハッシュ
0x1ac7d1b81b7ba1025b36ccb86723da6ee5a87259f1c2fd5abe69d3200b512ec8

ライブラリーの関数が何を入力として扱うかも確認します。たとえばethersのkeccak256はバイト表現を受け取り、UTF-8の文章を扱うidとは入口が違います。5

改行や正規化は、ハッシュの前に決める

abcabcの後ろに改行があるデータは別物です。シェルで短い検算をする場合は、出力が一見同じでも、末尾のLFが加わっていないか確認します。

printf '%s' 'abc' | openssl dgst -keccak-256
printf '%s\n' 'abc' | openssl dgst -keccak-256

文字列をtrim()して値が合ったとしても、それが正しい修正とは限りません。改行や空白を許す仕様なら、それもデータの一部だからです。まず入力の16進表記とバイト長を残し、どこで違いが生じたかを見ます。

見た目が同じ文字も同様です。このサンプルでは、éを一つのコードポイントで書いた値と、eに結合アクセントを続けた値は、UTF-8のバイト列もハッシュも異なりました。NFCで明示的に正規化した後は一致します。

import { utf8, keccak256 } from './hash-inputs.mjs';

const a = '\u00e9';
const b = 'e\u0301';
console.log(keccak256(utf8(a)) === keccak256(utf8(b))); // false
console.log(keccak256(utf8(a)) ===
  keccak256(utf8(b.normalize('NFC')))); // true

正規化をするかどうかは、ハッシュ関数ではなく入力仕様で決めます。この比較のために、既存プロトコルが定義するデータへ勝手な変換を追加しないようにします。

関数名が同じでも、シグネチャの文字列を省略しない

関数の識別子は、ABIが定める正規形のシグネチャをKeccak-256へ渡し、その先頭4バイトを取ります。引数名や戻り値は含めず、uintの正規形はuint256です。1

transfer(address,uint256) → 先頭4バイトは 0xa9059cbb
transfer(address,uint)    → 上と同じ文字列ではない

この2つをそのままハッシュすると、先頭4バイトも異なります。文字列を直接ハッシュする関数が、Solidityの型の省略形を自動展開してくれるとは考えないでください。

ただし、識別子が一致することと、呼び出し先の正当性は別の確認です。4バイトだけを見て、相手の実装や権限まで判断する用途には広げません。全体の読み方はABIから関数識別子・入力データ・イベントログを読む方法で整理しています。

不一致を調べるときに残すもの

まずアルゴリズム名を固定し、次にハッシュ直前のバイト列を16進表記と長さで比べます。その後に、文字コード、正規化、ABIの型と順序を確認します。

入力が一致しているのに出力が違う場合にはじめて、実装やバージョンの差を調べます。確認用の空入力と既知の関数シグネチャを一緒に残しておくと、「データの作り方」と「ハッシュ処理」を切り分けやすくなります。

ここで使ったのは公開してよい架空データだけです。実システムの入力を記録するときも、秘密鍵、署名前の機密情報、個人情報をログへ残す必要はありません。

参考資料

確認日:2026年9月5日。本文の計算例はローカル実行。Solidityのコンパイル、EVM実行、実ネットワークとの照合は行っていません。

Footnotes

  1. Solidity Contract ABI Specification: Function Selector 2

  2. OpenSSL KECCAK implementations 2

  3. OpenSSL SHA3 implementations 2

  4. Node.js Crypto: getHashes / createHash

  5. ethers v6 Cryptographic Functions

確認した一次情報