# eth\_getProof — Storage State Verification (SPV)

## 1. Introduction

Like Ethereum Merkle Proof verification, Pharos provides Simplified Payment Verification (SPV) to allow light clients to verify the existence — or non-existence — of accounts and storage values at a given block height, without possessing the full state.

Pharos implements the storage state trie using a **hexary hash tree** with the **SHA-256** hash algorithm. The trie consists of three node types:

| Node Type    | Size (bytes) | Structure                                                                                          |
| ------------ | ------------ | -------------------------------------------------------------------------------------------------- |
| **MSU Root** | 8192         | 256 × 32-byte SHA-256 hashes. The SHA-256 of this node equals the `stateRoot` in the block header. |
| **Internal** | 515          | 3-byte header + 16 × 32-byte child hashes.                                                         |
| **Leaf**     | 65           | 1-byte type + 32-byte key hash + 32-byte value hash.                                               |

The first-level subtree (MSU Root) contains 256 slots, with each slot representing a shard (partition) of the storage. The actual Merkle proof begins from the second-level subtree. Starting from the second-level subtree, each Internal node value starts with 3 bytes of metadata followed by 16 child node hashes. The hashes of child nodes at the corresponding offset positions in the parent node can be verified recursively, allowing us to determine whether the given root node hash can ultimately be computed, thus justifying the storage data at the specified block height.

<figure><img src="/files/b8rN0ZroWOOY7dfNIFVL" alt=""><figcaption><p>Pharos storage state trie</p></figcaption></figure>

<figure><img src="/files/8cBtPJ8hBc5mvfbI3jJp" alt=""><figcaption></figcaption></figure>

For more details of how Pharos implements SPV, you can check [SPV Proof Theory Explanation](/api-and-sdk/eth-getproof/spv-proof-theory.md)

***

## 2. API Reference

### `eth_getProof`

Returns the account information and Merkle proof for a given account address (and optionally specific storage keys) at a specified block height.

**Parameters:**

| # | Type       | Description                                                                                                      |
| - | ---------- | ---------------------------------------------------------------------------------------------------------------- |
| 1 | `string`   | Account address (20-byte hex, `0x`-prefixed)                                                                     |
| 2 | `string[]` | Array of storage keys to prove (each 32-byte hex, `0x`-prefixed). Pass `[]` if only the account proof is needed. |
| 3 | `string`   | Block number (hex `0x`-prefixed) or block tag (`"latest"`, `"earliest"`)                                         |

**curl Example — account proof only:**

```bash
curl -X POST http://<node-rpc-endpoint> \\
  -H "Content-Type: application/json" \\
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getProof",
    "params": [
      "0x4100000000000000000000000000000000000000",
      [],
      "latest"
    ]
  }'
```

**curl Example — account proof + storage proof:**

```bash
curl -X POST http://<node-rpc-endpoint> \\
  -H "Content-Type: application/json" \\
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getProof",
    "params": [
      "0x4100000000000000000000000000000000000000",
      ["0x0000000000000000000000000000000000000000000000000000000000000001"],
      "latest"
    ]
  }'
```

***

## 3. Response Format

### Common Fields

| Field          | Type       | Description                                                                                                        |
| -------------- | ---------- | ------------------------------------------------------------------------------------------------------------------ |
| `balance`      | `string`   | Account balance in hex. `"0x0"` if account does not exist.                                                         |
| `nonce`        | `string`   | Account nonce in hex. `"0x0"` if account does not exist.                                                           |
| `codeHash`     | `string`   | SHA-256 hash of the account's contract code. All-zero if none.                                                     |
| `storageHash`  | `string`   | Root hash of the account's storage trie.                                                                           |
| `isExist`      | `bool`     | `true` if the account exists at the given block, `false` otherwise.                                                |
| `accountProof` | `object[]` | Array of proof nodes from MSU Root to the target account. See [Proof Node Format](#proof-node-format).             |
| `storageProof` | `object[]` | Array of storage proof entries (one per requested storage key). See [Storage Proof Format](#storage-proof-format). |
| `rawValue`     | `string`   | Raw RLP-encoded account value (hex). Empty `"0x"` if account does not exist.                                       |

### Proof Node Format

Unlike Ethereum (which returns RLP-encoded hex strings), each Pharos proof node is a JSON object:

```json
{
  "proofNode": "0x<hex-encoded binary node data>",
  "nextBeginOffset": <int>,
  "nextEndOffset": <int>
}
```

* `proofNode`: The hex-encoded binary data of the trie node (MSU Root, Internal, or Leaf).
* `nextBeginOffset` / `nextEndOffset`: The byte offset range within this node's binary data that points to the **next** node in the proof chain. These offsets allow verifiers to locate the child hash slot without needing to know the internal addressing rules.

### Storage Proof Format

Each entry in `storageProof` has the following structure:

```json
{
  "key": "0x<storage-key>",
  "value": "0x<storage-value>",
  "isExist": true,
  "proof": [
    { "proofNode": "0x...", "nextBeginOffset": N, "nextEndOffset": M },
    ...
  ]
}
```

***

## 4. Existence Proof

When the queried account (or storage key) **exists**, the response contains a complete proof chain from the MSU Root node down to the Leaf node. The `isExist` field is `true`.

### Example Response (Existence)

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "balance": "0xD3C21BCECCEDA1000000",
    "nonce": "0x0",
    "codeHash": "0x415b1764c41b466152a1b0830e26f2e60c391b0dfe1e06eacf28522115f9041d",
    "storageHash": "0xccc11c21e5f76a72d68abcc8a72d3c50446342e12fd7b8cfe611955ed11baa24",
    "isExist": true,
    "rawValue": "0xee808ad3c21bcecceda100000080a0415b...",
    "accountProof": [
      { "proofNode": "0x<MSU Root: 8192 bytes>",    "nextBeginOffset": 1024, "nextEndOffset": 1056 },
      { "proofNode": "0x<Internal node: 515 bytes>", "nextBeginOffset": 35,   "nextEndOffset": 67   },
      { "proofNode": "0x<Internal node: 515 bytes>", "nextBeginOffset": 259,  "nextEndOffset": 291  },
      { "proofNode": "0x<Leaf node: 65 bytes>",      "nextBeginOffset": 0,    "nextEndOffset": 0    }
    ],
    "storageProof": [
      {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000001",
        "value": "0x<storage-value>",
        "isExist": true,
        "proof": [
          { "proofNode": "0x...", "nextBeginOffset": 35, "nextEndOffset": 67 },
          ...
        ]
      }
    ]
  }
}
```

### Verification Logic (Existence)

1. **Leaf verification**: The last proof node is a Leaf. Verify that `SHA256(key) == leaf.key_hash` and `SHA256(value) == leaf.value_hash`.
2. **Hash chain**: Compute `current_hash = SHA256(leaf_node)`. Walk upward through Internal nodes: confirm `current_hash` matches the slot at `[nextBeginOffset : nextEndOffset]` in the parent, then recompute `current_hash = SHA256(parent_node)`.
3. **Root verification**: The hash of the MSU Root node must equal the trusted `stateRoot` from the block header.

***

## 5. Non-existence Proof

When the queried account (or storage key) **does not exist**, the proof chain terminates at an Internal node where the target slot is all-zero (empty). The `isExist` field is `false`.

### Why Additional Sibling Proofs Are Needed

In an existence proof, the Leaf node's key hash and value hash provide a self-contained anchor: the verifier can compute hashes upward through the chain and confirm the root. However, for a non-existence proof, the chain terminates at an Internal node with an empty slot — and an Internal node filled with only zeros would itself hash to zero, which could be trivially forged.

To ensure the Internal node's integrity, the response includes **sibling leftmost leaf proofs** (`siblingLeftmostLeafProofs`). These provide proof paths from the non-empty sibling slots to their leftmost leaf nodes, enabling the verifier to:

1. Independently recompute each sibling slot's hash.
2. Reconstruct the full Internal node (including the empty target slot).
3. Verify the hash chain upward to the `stateRoot`.

This is a deliberate **trade-off**: the proof structure is more complex, but it enables compact storage optimization in the underlying trie engine. The verification semantics are complete — a non-existence proof fully demonstrates that the queried key is absent from the state at the given block height, and is sufficient for all practical use cases such as cross-chain bridge verification.

### `siblingLeftmostLeafProofs` Format

When `isExist` is `false` and the proof terminates at an Internal node with an empty slot, the response includes:

```json
{
  "siblingLeftmostLeafProofs": [
    {
      "slotIndex": 0,
      "leftmostLeafKey": "0x<key of the leftmost leaf in slot 0's subtree>",
      "proofPath": [
        { "proofNode": "0x<Internal node in slot 0's subtree>", "nextBeginOffset": N, "nextEndOffset": M },
        { "proofNode": "0x<Leaf node>", "nextBeginOffset": 0, "nextEndOffset": 0 }
      ]
    },
    {
      "slotIndex": 2,
      "leftmostLeafKey": "0x<key of the leftmost leaf in slot 2's subtree>",
      "proofPath": [
        { "proofNode": "0x<Leaf node>", "nextBeginOffset": 0, "nextEndOffset": 0 }
      ]
    }
  ]
}
```

Each entry represents a non-empty sibling slot of the terminating Internal node:

| Field             | Type       | Description                                                                                        |
| ----------------- | ---------- | -------------------------------------------------------------------------------------------------- |
| `slotIndex`       | `int`      | The slot index (0–15) of the sibling in the parent Internal node.                                  |
| `leftmostLeafKey` | `string`   | The key of the leftmost leaf node reachable from this sibling slot.                                |
| `proofPath`       | `object[]` | Proof path from the sibling slot down to the leftmost leaf. Same format as `accountProof` entries. |

### Example Response (Non-existence)

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "balance": "0x0",
    "nonce": "0x0",
    "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "storageHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "isExist": false,
    "rawValue": "0x",
    "accountProof": [
      { "proofNode": "0x<MSU Root: 8192 bytes>",    "nextBeginOffset": 1024, "nextEndOffset": 1056 },
      { "proofNode": "0x<Internal node: 515 bytes>", "nextBeginOffset": 259,  "nextEndOffset": 291  },
      { "proofNode": "0x<Internal: target slot is all-zero>", "nextBeginOffset": 35, "nextEndOffset": 67 }
    ],
    "siblingLeftmostLeafProofs": [
      {
        "slotIndex": 0,
        "leftmostLeafKey": "0x<sibling leaf key>",
        "proofPath": [
          { "proofNode": "0x<Internal>", "nextBeginOffset": 35, "nextEndOffset": 67 },
          { "proofNode": "0x<Leaf>",     "nextBeginOffset": 0,  "nextEndOffset": 0  }
        ]
      },
      {
        "slotIndex": 7,
        "leftmostLeafKey": "0x<sibling leaf key>",
        "proofPath": [
          { "proofNode": "0x<Leaf>", "nextBeginOffset": 0, "nextEndOffset": 0 }
        ]
      }
    ],
    "storageProof": []
  }
}
```

### Verification Logic (Non-existence)

1. **Main chain**: Walk the proof chain as usual. The last node is an Internal node — verify that the target slot (determined by the key hash nibble at the corresponding depth) is all-zero.
2. **Sibling verification**: For each entry in `siblingLeftmostLeafProofs`:
   * Concatenate the main proof chain (excluding the last node) with the sibling's `proofPath`.
   * Verify that this combined chain is a valid existence proof for `leftmostLeafKey`.
3. **Root verification**: Confirm the MSU Root node hashes to the trusted `stateRoot`.

***

## 6. Verification Script

A Python verification script [`spv_verify.py`](https://github.com/PharosNetwork/examples/blob/main/spv-verification/spv_verify.py) is provided to verify both existence and non-existence proofs. It handles:

* Automatic `stateRoot` computation from the MSU Root node (SHA-256) if not present in the JSON.
* Both existence proofs (leaf-terminated) and non-existence proofs (empty-slot-terminated with sibling verification).
* Optional cross-validation of `stateRoot` against an RPC endpoint.

### Usage

```bash
# Basic verification (no RPC cross-check):
python3 spv_verify.py proof.json --address 0x<account-address> --no-rpc

# With stateRoot cross-check against RPC:
python3 spv_verify.py proof.json --address 0x<account-address> \
    --block 0x<block-number> --rpc-url http://127.0.0.1:18100

# If the JSON already contains 'address' and 'stateRoot':
python3 spv_verify.py proof.json --no-rpc
```

The full script source is available at the [`examples` repository](https://github.com/PharosNetwork/examples/blob/main/spv-verification/spv_verify.py).

***

## 7. Comparison with Ethereum

For teams familiar with Ethereum's `eth_getProof` ([EIP-1186](https://eips.ethereum.org/EIPS/eip-1186)), the key differences are:

| Aspect                  | Ethereum                             | Pharos                                                          |
| ----------------------- | ------------------------------------ | --------------------------------------------------------------- |
| **Hash algorithm**      | Keccak-256                           | SHA-256                                                         |
| **Trie structure**      | Merkle Patricia Trie (MPT)           | Hexary hash tree with 256-slot MSU Root                         |
| **Node encoding**       | RLP-encoded hex strings              | Fixed-size binary objects                                       |
| **Proof format**        | `accountProof: string[]`             | `accountProof: object[]` (with `nextBeginOffset/nextEndOffset`) |
| **Non-existence proof** | Implicit (path terminates naturally) | Explicit `siblingLeftmostLeafProofs`                            |

**Why are non-existence proofs different?**

In Ethereum's MPT, nodes are RLP-encoded and self-describing — an empty branch is simply an empty string in the RLP list, and the verifier can decode and confirm it directly. In Pharos's hexary trie, Internal nodes are fixed-size binary structures where an empty slot is 32 zero-bytes. To prevent trivial forgery of all-zero Internal nodes, the non-existence proof includes sibling subtree proofs that allow the verifier to independently reconstruct and validate the Internal node's hash. This is a trade-off: proof verification is more complex, but the underlying storage engine benefits from significant optimizations. The verification semantics are complete and fully meet practical use case requirements.

For users who're interested in the implementation details of such proof and how it works, you can refer to [SPV Proof Theory Explanation](/api-and-sdk/eth-getproof/spv-proof-theory.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.pharos.xyz/api-and-sdk/eth-getproof.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
