# Ethereum RPCの0x・0x0・0x00は別物：QUANTITYとDATAの見分け方

Ethereum RPCの0x、0x0、0x00は同じゼロではありません。QUANTITYとDATAの形式、ABIの返り値、先頭ゼロ、空応答とnullを小さなパーサーで区別します。

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

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

Ethereum RPCの入力や応答には、`0x`で始まる文字列が多く登場します。しかし、見た目が似ていても、すべてを同じ16進数として処理してよいわけではありません。

**数量は整数の値を、DATAはバイト列とその長さを表します。** 数量の先頭ゼロを取り除く処理を、アドレスや呼び出しデータへそのまま適用すると、必要なバイトが失われます。[^eip1474]

本記事では、RPCへ実際に接続せず、ローカルの文字列だけでこの違いを確認します。

掲載例はNode.js 22以降で実行できます。各節のコードを`example.mjs`へ保存し、同じ節で使う関数定義と呼び出しを合わせてから`node example.mjs`を実行してください。再利用できる関数は[rpc-hex.mjs](/examples/offline-web3/rpc-hex.mjs)から取得できます。

## QUANTITYは数値、DATAはバイト列

EthereumのJSON-RPCでは、数量を表す`QUANTITY`と、バイト列を表す`DATA`を分けます。数量は余分な先頭ゼロを省き、ゼロは`0x0`です。一方、DATAは1バイトを16進数2桁で表すため、空なら`0x`、ゼロの1バイトなら`0x00`になります。[^eip1474]

| 表記 | QUANTITYとして | DATAとして |
| --- | --- | --- |
| `0x` | 不正。整数がない | 正しい。0バイト |
| `0x0` | 正しい。数量の0 | 不正。1桁しかない |
| `0x00` | 不正。余分な先頭ゼロ | 正しい。ゼロの1バイト |
| `0x1` | 正しい。数量の1 | 不正。奇数桁 |
| `0x01` | 不正。余分な先頭ゼロ | 正しい。値1の1バイト |
| `0x41` | 正しい。数量の65 | 正しい。値0x41の1バイト |

この表で重要なのは、`0x41`が両方の形式として成立することです。文字列を見るだけで型を確定することはできません。**どのRPCメソッドの、どのフィールドか**を先に決めます。

ここで参照するEIP-1474の状態はStagnantであり、Finalの仕様とは呼びません。現在のExecution APIの公開スキーマにも、数量、任意長バイト列、20バイトや32バイトの値を区別したスキーマがあります。実装では、対象メソッドのスキーマも合わせて確認します。なお、確認時点のExecution APIの汎用`bytes`パターン自体は偶数桁まで制限していません。本記事のパーサーはEIP-1474の1バイト2桁という方針を明示的に検証する例であり、スキーマをそのまま写したものではありません。[^eip1474][^schemas]

## eth_getBalanceの結果とeth_callの結果を同じパーサーへ入れない

代表的な違いは、`eth_getBalance`と`eth_call`です。前者の返り値はwei単位の数量ですが、後者の返り値はコントラクトの実行が返したDATAです。`eth_getCode`の結果もバイト列です。[^rpc]

たとえば、ABIで`uint256`を1つ返す関数がゼロを返した場合、ABI形式の返り値は32バイトのゼロになります。`0x0`という数量表現をそのまま返すわけではありません。[^abi]

```text
数量の0             → 0x0
空の返り値          → 0x
uint256(0)のABI表現  → 0x の後ろにゼロ64桁
```

`uint256`が返ることを期待しているのに`0x`だった場合、ゼロ残高として補完するのは避けます。「期待する32バイトがない」という別の問題として扱います。

アドレス、呼び出し先、ABI、取得したブロックなどを確認する余地はありますが、空データだけで原因を決めつける必要もありません。まず、応答形式と期待する型の不一致を保存します。

## 汎用のhex整形関数を作らず、型ごとに分ける

数量をBigIntへ変換する関数と、DATAをバイト列へ変換する関数を分離します。以下のサンプルは、入力表記を小文字の`0x`形式に統一する方針です。

```js
function readQuantity(value) {
  if (typeof value !== 'string' ||
      !/^0x(?:0|[1-9a-f][0-9a-f]*)(?![\s\S])/.test(value)) {
    throw new TypeError('invalid lowercase QUANTITY');
  }
  return BigInt(value);
}

function readData(value, expectedBytes) {
  if (typeof value !== 'string' ||
      !/^0x(?:[0-9a-f]{2})*(?![\s\S])/.test(value)) {
    throw new TypeError('invalid lowercase DATA');
  }
  if (expectedBytes !== undefined &&
      (!Number.isSafeInteger(expectedBytes) || expectedBytes < 0)) {
    throw new RangeError('invalid expected byte length');
  }
  const length = (value.length - 2) / 2;
  if (expectedBytes !== undefined && length !== expectedBytes) {
    throw new RangeError('unexpected byte length');
  }
  return Buffer.from(value.slice(2), 'hex');
}
```

`(?![\s\S])`は、その位置より後に文字が残っていないことを確認する末尾条件です。JavaScriptでは`m`フラグなしの`$`も入力末尾だけに一致しますが、この例ではフラグに依存しない形で全体一致を表しています。末尾改行は受け入れません。[^regexp]

この検証は値全体を正規表現で確認してから変換します。Node.jsの`Buffer.from(value, 'hex')`は、不正な文字や途中の半バイトに対して、入力を途中までで解釈することがあります。そのため、変換処理に入力検証を兼ねさせていません。[^buffer]

```js
readQuantity('0x41');      // 65n
readQuantity('0x01');      // TypeError
readData('0x').length;     // 0
readData('0x00').length;   // 1
readData('0x0001').length; // 2
```

本稿のパーサーは形式と任意の長さを検査する小さな例です。数量のフィールド固有の上限、JSON-RPC応答全体の構造、許容サイズ、アドレスのチェックサムまで検証するものではありません。

## DATAを数値へ変えると、先頭ゼロと長さが消える

整数としては、次の2つは同じです。

```js
BigInt('0x0001') === BigInt('0x01'); // true
```

しかし、バイト列としては違います。

```text
0x0001 → 00 01 → 2バイト
0x01   → 01    → 1バイト
```

したがって、いったんBigIntへ変換し、`toString(16)`で戻す処理は、任意のDATAを保存する方法にはなりません。

この違いをアドレスへ当てはめると、20バイトの先頭がゼロだった場合にも、そのゼロはアドレス表現の一部として必要です。32バイトのハッシュも、数量のように短くする対象ではありません。Execution APIのスキーマでは、これらの長さを別々に指定しています。[^schemas]

「ゼロを消すと短くなって見やすい」という表示上の都合と、プロトコルへ渡すバイト列の保存は分けて考えます。

## nullはゼロでも、空のバイト列でもない

`eth_getTransactionReceipt`は、レシートが見つからない場合に`null`を返すことがあります。これは`status`が失敗を表すレシートとは異なります。[^rpc]

```json
{"jsonrpc":"2.0","id":1,"result":null}
```

この応答を`0x`へ置き換えてからデコードしたり、`0x0`へ変換して失敗扱いにしたりすると、「見つかっていない」と「失敗して記録された」を混同します。

アプリ側では、応答エラー、`result: null`、空DATA、数量ゼロを別の状態として保存するのがよい設計です。異常系を一律にゼロへ落とすと、後から原因を追えなくなります。

## まとめ

RPCの16進文字列は、次の順に読みます。

1. メソッドとフィールドを確認する。
2. QUANTITYかDATAかを決め、形式を検証する。
3. 必要な長さや範囲を確かめてから変換する。

数量にはBigInt、バイト列にはバイト列の型を使い、どちらも`null`と混同しません。`0x`、`0x0`、`0x00`を別のテストケースにするだけでも、空応答や先頭ゼロを誤処理する経路を見つけやすくなります。

### 検証メモ

例はNode.js v22.16.0とv24.14.0で確認しました。空のバイト列、ゼロ数量、先頭ゼロを持つバイト列を区別し、不正な表記を拒否します。実RPCへの要求や、各プロバイダー独自の許容表記は確認していません。

関連記事：[Ethereum RPCのlatestがずれる理由](/archives/6024)、[ABIから関数識別子・入力データ・イベントログを読む方法](/archives/6005)

[^eip1474]: [EIP-1474 — Value encoding](https://eips.ethereum.org/EIPS/eip-1474#value-encoding)。参照日：2026-09-05。状態：Stagnant。
[^schemas]: [Ethereum Execution APIs — base-types.yaml](https://github.com/ethereum/execution-apis/blob/main/src/schemas/base-types.yaml)。参照日：2026-09-05。
[^rpc]: [Ethereum.org — JSON-RPC API](https://ethereum.org/en/developers/docs/apis/json-rpc/)。参照日：2026-09-05。
[^abi]: [Solidity — Contract ABI Specification](https://docs.soliditylang.org/en/latest/abi-spec.html)。参照日：2026-09-05。
[^buffer]: [Node.js — Buffer / hexadecimal encoding](https://nodejs.org/api/buffer.html)。参照日：2026-09-05。
[^regexp]: [ECMAScript 2025：正規表現の入力境界](https://tc39.es/ecma262/2025/multipage/text-processing.html#sec-assertion)。参照日：2026-09-06。

## 確認した一次情報

- [EIP-1474 — Value encoding](<https://eips.ethereum.org/EIPS/eip-1474#value-encoding>): 確認日 2026-09-05
- [Ethereum Execution APIs — base-types.yaml](<https://github.com/ethereum/execution-apis/blob/main/src/schemas/base-types.yaml>): 確認日 2026-09-05
- [Ethereum.org — JSON-RPC API](<https://ethereum.org/en/developers/docs/apis/json-rpc/>): 確認日 2026-09-05
- [Solidity — Contract ABI Specification](<https://docs.soliditylang.org/en/latest/abi-spec.html>): 確認日 2026-09-05
- [Node.js — Buffer / hexadecimal encoding](<https://nodejs.org/api/buffer.html>): 確認日 2026-09-05
- [ECMAScript 2025：正規表現の入力境界](<https://tc39.es/ecma262/2025/multipage/text-processing.html#sec-assertion>): 確認日 2026-09-06
