3MIKAN
仮想通貨直コン

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

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

3MIKANのキャラクターが検索用の札と原文の手紙を別の引き出しへ整理する場面

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

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

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

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

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

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

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

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

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

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

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

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

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));
synthetic-abi-log-not-a-receipt
0xeb687308225e4cab27dd7768e7f14b81f97eab5ac3aedeb0e1ce58fc4a31c7de
hello

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

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

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

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

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

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

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

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

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

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仕様にも、この分離が示されています。1

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

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

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

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

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

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

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

まとめ

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

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

参考資料

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

Footnotes

  1. Solidity ABI: Events 2 3 4 5

  2. Solidity ABI: Encoding of Indexed Event Parameters 2 3

  3. Solidity ABI: Formal Specification of the Encoding

  4. Solidity Contracts: Events

確認した一次情報