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

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

正規URL: https://3mikan.com/archives/6075

著者: みかん
公開: 2026-09-06T00:00:00.000Z
更新: 2026-09-06T00:00:00.000Z

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

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

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

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

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

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

```text
空入力の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でも同じ結果を確認しました。これは実行した環境であり、最新版の案内ではありません。

```bash
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.mjs`も`PATH`から選ばれるコマンドを使います。エラーを消すために`sha3-256`へ置き換えてはいけません。[^source2] [^source3]

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

```js
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バイト |

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

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

```js
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));
```

```text
2バイトをハッシュ
0x56570de287d73cd1cb6092bb8fdee6173974955fdef345ae579ee9f475ea7432

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

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

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

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

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

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

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

```js
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`です。[^source1]

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

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

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

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

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

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

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

## 参考資料

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

[^source1]: [Solidity Contract ABI Specification: Function Selector](https://docs.soliditylang.org/en/v0.8.36/abi-spec.html)
[^source2]: [OpenSSL KECCAK implementations](https://docs.openssl.org/3.5/man7/EVP_MD-KECCAK/)
[^source3]: [OpenSSL SHA3 implementations](https://docs.openssl.org/3.5/man7/EVP_MD-SHA3/)
[^source4]: [Node.js Crypto: getHashes / createHash](https://nodejs.org/docs/latest-v22.x/api/crypto.html)
[^source5]: [ethers v6 Cryptographic Functions](https://docs.ethers.org/v6/api/crypto/)

## 確認した一次情報

- [Solidity Contract ABI Specification: Function Selector](<https://docs.soliditylang.org/en/v0.8.36/abi-spec.html>): 確認日 2026-09-05
- [OpenSSL KECCAK implementations](<https://docs.openssl.org/3.5/man7/EVP_MD-KECCAK/>): 確認日 2026-09-05
- [OpenSSL SHA3 implementations](<https://docs.openssl.org/3.5/man7/EVP_MD-SHA3/>): 確認日 2026-09-05
- [Node.js Crypto: getHashes / createHash](<https://nodejs.org/docs/latest-v22.x/api/crypto.html>): 確認日 2026-09-05
- [ethers v6 Cryptographic Functions](<https://docs.ethers.org/v6/api/crypto/>): 確認日 2026-09-05
