# indexed stringはなぜログから読めない？検索用トピックと原文を分ける

イベントのindexed文字列がハッシュになる理由を、topicsとdataの違い、候補値の照合、通常のABI符号化との違いから説明します。実アカウントを使わず合成ログで検算します。

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

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

イベントに文字列を渡したのに、ログを読むと長い16進数しか出てこない。ABIを渡しても元の文章へ戻らない。まず確認したいのは、その引数に**`indexed`が付いているか**です。

この記事は、イベントを読む開発者と、検索しやすいイベントを設計したい人向けです。検索用のトピックと、文字列を残すデータ領域の役割を分けます。使うログはローカルで作った合成データで、実コントラクトが出力したレシートではありません。

掲載例はNode.js 22以降で実行できます。[hash-inputs.mjs](/examples/offline-protocol-reading/hash-inputs.mjs)と[event-string.mjs](/examples/offline-protocol-reading/event-string.mjs)を同じフォルダーへ保存し、各節のJavaScriptを別の`example.mjs`へ写して`node example.mjs`を実行してください。ハッシュ計算にはKECCAK-256対応のOpenSSLコマンドも必要です。[ハッシュ計算の実行環境を確かめる手順](/archives/6075)で、実際に使うコマンドを先に確認します。

## indexedは検索用の場所へ値を置く指定

通常の、`anonymous`ではないイベントを考えます。

```solidity
// 宣言の説明例。この記事ではコンパイル・emitを実行しない。
event Message(string indexed tag, string body);
```

この宣言では`tag`と`body`の保存のされ方が異なります。Solidityのイベント仕様[^source1]では、`indexed`の引数はトピックへ、それ以外の引数はABI符号化された`data`へ配置します。

| ログの場所 | この宣言での内容 |
| --- | --- |
| `topics[0]` | `Message(string,string)`のKeccak-256 |
| `topics[1]` | `tag`のUTF-8内容をKeccak-256へ渡した値 |
| `data` | `body`を一つの`string`引数としてABI符号化した値 |

ここで対象にしているのは、イベントへ直接渡す`string`です。配列や構造体の索引化に使う符号化を、同じ単純な文字列連結だとは考えないでください。[^source2]

## 短い文字列でも、stringならハッシュになる

トピックの1枠は32バイトですが、4文字の`memo`だから原文がそのまま入る、という動作にはなりません。`string`は動的型なので、`indexed`なら原文ではなくハッシュが入ります。一方、たとえば`bytes32`という固定長型には別の符号化規則があります。[^source1] [^source2]

この違いを、`tag = "memo"`、`body = "hello"`で確認します。付属の`event-string.mjs`を使うと、実ネットワークに接続せずに三つの欄を組み立てられます。

```js
import { syntheticMessageLog, decodeOneString } from './event-string.mjs';

const log = syntheticMessageLog('memo', 'hello');
console.log(log.evidenceType);
console.log(log.topics[1]);
console.log(decodeOneString(log.data));
```

```text
synthetic-abi-log-not-a-receipt
0xeb687308225e4cab27dd7768e7f14b81f97eab5ac3aedeb0e1ce58fc4a31c7de
hello
```

`hello`は`data`側から読めます。しかし、`topics[1]`を文字コードとして解釈しても、そこに`memo`のUTF-8原文が保存されているわけではありません。

この合成ログはバイト配置を学ぶためのものです。発行元アドレス、ブロック、トランザクション、実行結果を証明しません。

## 候補を照合することと、元へ戻すことは違う

元の文字列が分からない状態でハッシュから文章を読み戻すことと、「この値はmemoか」と候補を計算して比べることは別です。

```js
import { syntheticMessageLog, candidateMatchesStringTopic } from './event-string.mjs';

const topic = syntheticMessageLog('memo', 'hello').topics[1];
console.log(candidateMatchesStringTopic('memo', topic));  // true
console.log(candidateMatchesStringTopic('other', topic)); // false
```

索引化された動的型は、候補を既に知っている検索には使えます。ただし、そのトピックだけで任意の原文をデコードできるわけではありません。[^source1]

また、ハッシュで残したから秘密になったとも考えません。たとえば候補が`memo`と`other`の二つしかなければ、上のように両方を試せます。個人情報や秘密の文字列を入れるかどうかは、検索しやすさとは別に判断します。

## keccak256(abi.encode(tag))とは入力が違う

ここは間違えやすい箇所です。直接の`indexed string`は、文字列の内容を長さやパディングなしで符号化します。通常の`abi.encode(string)`では、動的型のオフセット、バイト長、パディングが関係するため、同じ文字列でもハッシュ前のバイト列が異なります。[^source2] [^source3]

```js
import { keccak256, utf8, fromHex } from './hash-inputs.mjs';
import { encodeOneString } from './event-string.mjs';

const contentHash = keccak256(utf8('memo'));
const abiHash = keccak256(fromHex(encodeOneString('memo')));
console.log(contentHash === abiHash); // false
```

一つの`string`として`hello`をABI符号化すると、今回の例は96バイトになります。最初の32バイトがオフセット32、次の32バイトがUTF-8の長さ5、その後が`68 65 6c 6c 6f`と27バイトのゼロ埋めです。

付属の`decodeOneString`は、この単一文字列の配置だけを読む学習用コードです。オフセット、長さ、末尾のゼロ埋め、不正なUTF-8を検査しますが、タプルや配列まで扱う汎用ABIデコーダーではありません。

## 原文も必要なら、別の非indexed引数へ残す

検索と原文表示の両方が要る場合は、同じ値を索引化する引数と、原文を保存する引数を分ける設計が考えられます。SolidityのABI仕様にも、この分離が示されています。[^source1]

```solidity
// 宣言の設計例。送信・デプロイ用の手順ではない。
event MessageRecorded(
    string indexed tag,
    string tagPlain,
    string body
);
```

この宣言だけでは、`tag`と`tagPlain`が同じになる保証はありません。実装側で同じ値を渡し、読む側でも「原文をハッシュし直した結果が検索用トピックと一致するか」を確認するのが検証方法になります。

原文を追加すれば保存するデータも増えます。この記事ではガス使用量を測っていないため、安くなる、何円増えるといった比較はしません。原文を公開する必要性と、保存する情報の範囲を先に決めます。

## `topics[0]`だけで、イベントの型を断定しない

イベントの識別文字列には、名前と正規形の引数型が入ります。`indexed`の有無や引数名自体は入りません。そのため、索引化する引数の位置が違っても、名前と型の並びが同じなら同じ識別トピックになります。[^source1]

さらに`anonymous`イベントでは、通常のイベント識別用トピックがありません。実際のログを解釈するときは、発行元、対応するABI、`indexed`の位置、`anonymous`の指定をそろえる必要があります。[^source4]

既知のトピック文字列が見つかったという理由だけで、特定の実装や操作結果まで決めつけないようにします。ABI全体の読み方は[入力データとイベントログの解説](/archives/6005)へつながります。

## まとめ

`indexed string`で残るのは、原文ではなく検索用のハッシュです。候補をハッシュして一致を確かめることはできますが、任意の原文をその欄から読むこととは違います。

トピックの計算が合わないときは、まず直接の文字列をハッシュしているか、通常のABI符号化まで入れていないかを確認します。原文表示も要る場合は、別の非索引引数へ残す設計を検討します。

## 参考資料

確認日：2026年9月5日。Node.js v22.16.0とOpenSSL CLI 3.5.5でハッシュ・合成ABIデータを検算しました。Solidityコンパイル、EVM、実ログ取得は未実施です。

[^source1]: [Solidity ABI: Events](https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#events)
[^source2]: [Solidity ABI: Encoding of Indexed Event Parameters](https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#encoding-of-indexed-event-parameters)
[^source3]: [Solidity ABI: Formal Specification of the Encoding](https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#formal-specification-of-the-encoding)
[^source4]: [Solidity Contracts: Events](https://docs.soliditylang.org/en/v0.8.36/contracts.html#events)

## 確認した一次情報

- [Solidity ABI: Events](<https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#events>): 確認日 2026-09-05
- [Solidity ABI: Encoding of Indexed Event Parameters](<https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#encoding-of-indexed-event-parameters>): 確認日 2026-09-05
- [Solidity ABI: Formal Specification of the Encoding](<https://docs.soliditylang.org/en/v0.8.36/abi-spec.html#formal-specification-of-the-encoding>): 確認日 2026-09-05
- [Solidity Contracts: Events](<https://docs.soliditylang.org/en/v0.8.36/contracts.html#events>): 確認日 2026-09-05
