---
title: "GenLayer Node API"
description: "The GenLayer Node provides a JSON-RPC API for interacting with it. This API allows you to execute contract calls, retrieve transaction information, and perform…"
source: https://docs.genlayer.com/api-references/genlayer-node
last_updated: 2026-10-20
---

# GenLayer Node API

The GenLayer Node provides a [JSON-RPC API](https://www.jsonrpc.org/specification) for interacting with it. This API allows you to execute contract calls, retrieve transaction information, and perform various blockchain operations.

## GenLayer Methods

### gen_call

Executes a new message call immediately without creating a transaction on the blockchain. This method supports read operations, write operations, and contract deployments.

**Method:** `gen_call`

**Parameters:**

- `request` (object, required): The contract call request parameters
  - `from` (string, required): The address making the call
  - `to` (string, required): The contract address to call
  - `data` (string, required): The encoded function call data
  - `type` (string, required): The type of call (`"read"`, `"write"`, or `"deploy"`)
  - `blockNumber` (string, optional): The block number to query at (hex-encoded). If not provided, uses latest block
  - `status` (string, optional): The transaction status filter (`"finalized"` or `"decided"`). If not provided, uses decided state
  - `value` (string, optional): The value to send with the call (hex-encoded)
  - `leader_public_data` (string or null, optional): Hex-encoded data returned by a previous leader execution. Its encoding is defined by the GenVM specification. When provided, GenVM runs in validator mode; when omitted or null, GenVM runs in leader mode

**Returns:** A structured JSON object containing execution results and debug information

**Response Fields:**

- `data` (string): Hex-encoded return value from contract execution
- `leaderPublicData` (string): Hex-encoded leader data whose encoding is defined by the GenVM specification. Populated in leader mode; empty in validator mode
- `status` (object): Execution status
  - `code` (number): Status code (0=success, 1=user error, 2=vm error, 3=internal error)
  - `message` (string): Human-readable status message
- `stdout` (string): Standard output captured during execution
- `stderr` (string): Standard error captured during execution
- `logs` (array): GenVM log entries, each as a JSON object with fields like `level`, `message`, `target`, `file`, `ts`
- `events` (array): Contract events emitted during execution
  - `topics` (array of strings): Event topics as hex-encoded strings
  - `data` (string): Event data as hex-encoded string
- `messages` (array): Messages emitted by the contract
  - `messageType` (number): Type identifier for the message
  - `recipient` (string): Recipient address
  - `value` (string): Value in hex format
  - `data` (string): Message data as hex-encoded string
  - `onAcceptance` (boolean): Whether message is sent on acceptance. Always `false` for an external message (`messageType` 0): external messages on acceptance are not supported yet
  - `saltNonce` (string): Salt nonce in hex format
- `nondetDisagreementCallNo` (number or null): When running in validator mode (with `leader_public_data`), indicates which `run_nondet` call the validator disagreed on. Null when there is no disagreement or when running in leader mode
- `syncedBlock` (string): Hex-encoded block number that GenVM has synced to. Indicates data freshness — responses are based on state up to this block

**Example - Deploy Call:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_call",
  "params": [
    {
      "type": "deploy",
      "data": "0xf9023db90223232076302e312e300a23207b2022446570656e6473223a202270792d67656e6c617965723a3135716669766a7679383038303072683939387063786d64326d38766131777132717a71687a3835306e3867676372346939713022207d0a0a66726f6d2067656e6c6179657220696d706f7274202a0a0a0a2320636f6e747261637420636c6173730a636c6173732053746f7261676528676c2e436f6e7472616374293a0a2020202073746f726167653a207374720a0a202020202320636f6e7374727563746f720a20202020646566205f5f696e69745f5f2873656c662c20696e697469616c5f73746f726167653a20737472293a0a202020202020202073656c662e73746f72616765203d20696e697469616c5f73746f726167650a0a20202020232072656164206d6574686f6473206d75737420626520616e6e6f7461746564207769746820766965770a2020202040676c2e7075626c69632e766965770a20202020646566206765745f73746f726167652873656c6629202d3e207374723a0a202020202020202072657475726e2073656c662e73746f726167650a0a2020202023207772697465206d6574686f640a2020202040676c2e7075626c69632e77726974650a20202020646566207570646174655f73746f726167652873656c662c206e65775f73746f726167653a2073747229202d3e204e6f6e653a0a202020202020202073656c662e73746f72616765203d206e65775f73746f726167650a950e04617267730d6c696e697469616c2076616c756500",
      "from": "0xf6A60752C24Cd3BFcAf8d387a5EB62d3746F44EE",
      "to": "0x0000000000000000000000000000000000000000",
      "status": "finalized"
    }
  ],
  "id": 1
}
```

**Example Response (Deploy Success):**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "data": "00",
    "leaderPublicData": "0e076e645f6f75747305",
    "status": {
      "code": 0,
      "message": "success"
    },
    "stdout": "",
    "stderr": "",
    "logs": [
      {
        "file": "common/src/lib.rs:111",
        "level": "info",
        "message": "logging initialized",
        "target": "genvm_common",
        "ts": "1770221913210",
        "version": "v0.2.11-aarch64-macos-release"
      },
      {
        "file": "src/exe/run.rs:150",
        "genvm_id": "1",
        "level": "info",
        "message": "genvm id",
        "target": "genvm::exe::run",
        "ts": "1770221913212"
      },
      {
        "file": "src/lib.rs:299",
        "level": "info",
        "message": "metrics",
        "metrics": {
          "gvm": {
            "host": { "time": 654 },
            "llm_module": { "calls": 0, "time": 0 },
            "supervisor": { "compilation_time": 0, "compiled_modules": 0, "precompile_hits": 2 },
            "web_module": { "calls": 0, "time": 0 }
          },
          "llm": null,
          "web": null
        },
        "target": "genvm",
        "ts": "1770221913590"
      }
    ],
    "events": [],
    "messages": [],
    "nondetDisagreementCallNo": null
  },
  "id": 1
}
```

**Example - Read Call (Latest Decided State):**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_call",
  "params": [
    {
      "type": "read",
      "data": "0xd8960e066d6574686f646c6765745f686176655f636f696e00",
      "from": "0xf6A60752C24Cd3BFcAf8d387a5EB62d3746F44EE",
      "to": "0xCe5712E63fd5475288aB1B7c0a368B9417357b81",
      "gas": "0x5208",
      "value": "0x0"
    }
  ],
  "id": 1
}
```

**Example - Read Call with Specific Block and Status:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_call",
  "params": [
    {
      "type": "read",
      "data": "0xd8960e066d6574686f646c6765745f686176655f636f696e00",
      "from": "0xf6A60752C24Cd3BFcAf8d387a5EB62d3746F44EE",
      "to": "0xCe5712E63fd5475288aB1B7c0a368B9417357b81",
      "blockNumber": "0x151ec5",
      "status": "decided"
    }
  ],
  "id": 1
}
```

**Example - Write Call:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_call",
  "params": [
    {
      "from": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6",
      "to":   "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6",
      "data": "0xa9059cbb000000000000000000000000742d35cc6634c0532925a3b8d4c9db96c4b4d8b6",
      "type": "write",
      "gas":  "0x5208",
      "value":"0x0"
    }
  ],
  "id": 1
}
```

**Example Response with Messages (Router Forward Call):**

When a contract emits internal messages (e.g., a router contract forwarding calls), the response includes the messages array:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "data": "00",
    "leaderPublicData": "0e076e645f6f75747305",
    "status": {
      "code": 0,
      "message": "success"
    },
    "stdout": "",
    "stderr": "",
    "logs": [
      {
        "file": "src/lib.rs:299",
        "level": "info",
        "message": "metrics",
        "target": "genvm",
        "ts": "1770225770000"
      }
    ],
    "events": [],
    "messages": [
      {
        "messageType": 1,
        "recipient": "0xc346f80c86adf1d71accfc4ef6efe7482b25be40",
        "value": "0x0",
        "data": "0xf86eb86a16046172677331d4023078393164463338654243623930384335643635...",
        "onAcceptance": false,
        "saltNonce": "0x0"
      },
      {
        "messageType": 1,
        "recipient": "0xad26513a4315b0ea8a5c800322b35092a83dfd3d",
        "value": "0x0",
        "data": "0xf86eb86a16046172677331d4023078333738653339374533316346383538333630...",
        "onAcceptance": false,
        "saltNonce": "0x0"
      }
    ],
    "nondetDisagreementCallNo": null
  },
  "id": 1
}
```

**Notes:**

- The `blockNumber` parameter can be either hex-encoded with a `0x` prefix (e.g., `"0x151ec5"`) or decimal (e.g., `"1384133"`)
- Valid `status` values are `"decided"` (default) and `"finalized"`
- When both parameters are omitted, the call executes against the latest decided state
- For `read` and `write` type calls, if the contract doesn't exist at the specified block/status combination, an error is returned with code `-32001` (Resource not found) and message `"contract not found at address 0x..."`
- For `deploy` type calls, the `status` and `blockNumber` parameters are not used for contract state validation since no contract exists yet

### gen_getContractSchema

Retrieve the schema/interface of a GenLayer contract to understand its available methods and properties.

**Method:** `gen_getContractSchema`

**Parameters:**

- `request` (object, required): The contract schema request
  - `code` (string, required): Base64-encoded contract code

**Returns:** Contract schema object with methods and properties

```json
{
  "ctor": { // constructor info
    "kwparams": {}, // dict
    "params": [["value", "type"]] // list of tuples
  },
  "methods": { // dict from method name to method info
    "method_name": {
      "kwparams": {}, // dict
      "params": [["value", "type"]], // list of tuples
      "payable": false, // bool
      "readonly": false, // bool
      "ret": "null" // string
    },
  }
}
```

**Example:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getContractSchema",
  "params": [
    {
      "code": "IyB7ICJEZXBlbmRzIjogInB5LWdlbmxheWVyOnRlc3QiIH0KCmZyb20gZ2VubGF5ZXIgaW1wb3J0ICoKCgojIGNvbnRyYWN0IGNsYXNzCmNsYXNzIFN0b3JhZ2UoZ2wuQ29udHJhY3QpOgogICAgc3RvcmFnZTogc3RyCgogICAgIyBjb25zdHJ1Y3RvcgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGluaXRpYWxfc3RvcmFnZTogc3RyKToKICAgICAgICBzZWxmLnN0b3JhZ2UgPSBpbml0aWFsX3N0b3JhZ2UKCiAgICAjIHJlYWQgbWV0aG9kcyBtdXN0IGJlIGFubm90YXRlZCB3aXRoIHZpZXcKICAgIEBnbC5wdWJsaWMudmlldwogICAgZGVmIGdldF9zdG9yYWdlKHNlbGYpIC0+IHN0cjoKICAgICAgICByZXR1cm4gc2VsZi5zdG9yYWdlCgogICAgIyB3cml0ZSBtZXRob2QKICAgIEBnbC5wdWJsaWMud3JpdGUKICAgIGRlZiB1cGRhdGVfc3RvcmFnZShzZWxmLCBuZXdfc3RvcmFnZTogc3RyKSAtPiBOb25lOgogICAgICAgIHNlbGYuc3RvcmFnZSA9IG5ld19zdG9yYWdlCg=="
    }
  ],
  "id": 1
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "ctor": {
      "kwparams": {},
      "params": [["initial_storage", "string"]]
    },
    "methods": {
      "get_storage": {
        "kwparams": {},
        "params": [],
        "readonly": true,
        "ret": "string"
      },
      "update_storage": {
        "kwparams": {},
        "params": [["new_storage", "string"]],
        "payable": false,
        "readonly": false,
        "ret": "null"
      }
    }
  },
  "id": 1
}
```

### gen_getContractState

Retrieves the current state of a GenLayer intelligent contract at a specific block height and transaction status.

**Method:** `gen_getContractState`

**Parameters:**

- `request` (object, required): The contract state request parameters
  - `address` (string, required): The contract address to query
  - `blockNumber` (string, optional): The block number to query at (hex-encoded). If not provided, uses latest block
  - `status` (string, optional): The transaction status filter (`"finalized"` or `"decided"`). If not provided, uses decided state

**Returns:** Hex-encoded contract state data

**Limits:**

| Limit | Default | Config key |
|---|---|---|
| Contract bytes read per call | 16 MB | `node.rpc.contract_read_max_bytes` |

This method reads the contract's whole storage, so its cost grows with the contract. The limit stops the read once the contract exceeds it and returns an error, rather than returning part of the state as if it were all of it. Operators may reduce it; `0` or less disables it.

**Example Request - Latest Decided State:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getContractState",
  "params": [
    {
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f"
    }
  ],
  "id": 1
}
```

**Example Request - Specific Block and Status:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getContractState",
  "params": [
    {
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "blockNumber": "0x151ec5",
      "status": "decided"
    }
  ],
  "id": 1
}
```

**Example Response:**

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

**Notes:**

- The `blockNumber` parameter should be hex-encoded with a `0x` prefix
- Valid `status` values are `"decided"` (default) and `"finalized"`
- The returned state is hex-encoded binary data with a `0x` prefix
- Empty contract state returns `"0x"`
- If the contract doesn't exist at the specified block/status, an error will be returned

**cURL Example:**

```bash
# Get latest decided state
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getContractState",
    "params": [{
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f"
    }],
    "id": 1
  }'

# Get state at specific block with decided status
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getContractState",
    "params": [{
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "blockNumber": "0x151ec5",
      "status": "decided"
    }],
    "id": 1
  }'
```

### gen_getContractCode

Retrieves the code of a deployed GenLayer intelligent contract at a specific block height and transaction status.

**Method:** `gen_getContractCode`

**Parameters:**

- `request` (object, required): The contract code request parameters
  - `address` (string, required): The contract address to query
  - `blockNumber` (string, optional): The block number to query at (hex-encoded). If not provided, uses latest block
  - `status` (string, optional): The transaction status filter (`"finalized"` or `"decided"`). If not provided, uses decided state

**Returns:** Base64-encoded contract code

**Limits:**

| Limit | Default | Config key |
|---|---|---|
| Contract bytes read per call | 16 MB | `node.rpc.contract_read_max_bytes` |

The node reads the contract's code slot directly, so the cost of a call is proportional to the code it returns rather than to everything the contract stores. The limit is a backstop above that: a contract whose declared code length exceeds it is refused before the bytes are read. Operators may reduce it; `0` or less disables it.

**Example Request - Latest Decided State:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getContractCode",
  "params": [
    {
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f"
    }
  ],
  "id": 1
}
```

**Example Request - Specific Block and Status:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getContractCode",
  "params": [
    {
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "blockNumber": "0x151ec5",
      "status": "finalized"
    }
  ],
  "id": 1
}
```

**Example Response:**

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

**Notes:**

- The `blockNumber` parameter should be hex-encoded with a `0x` prefix
- Valid `status` values are `"decided"` (default) and `"finalized"`
- The returned code is base64-encoded
- If the contract doesn't exist at the specified block/status, an error will be returned
- If the address does not have code (not a contract), an error will be returned
- If the contract's code exceeds `node.rpc.contract_read_max_bytes`, the call is refused rather than truncated, so a partial answer is never returned as a whole one

**cURL Example:**

```bash
# Get latest decided code
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getContractCode",
    "params": [{
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f"
    }],
    "id": 1
  }'

# Get code at specific block with finalized status
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getContractCode",
    "params": [{
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "blockNumber": "0x151ec5",
      "status": "finalized"
    }],
    "id": 1
  }'
```

### gen_getTransactionReceipt

Retrieves the transaction receipt for a given transaction ID. This endpoint is a proxy to the underlying consensus layer's `getTransactionAllData` function, providing detailed information about a transaction that has been processed.

**Method:** `gen_getTransactionReceipt`

**Parameters:**

- `request` (object, required):
  - `txId` (`string`, required): The 32-byte transaction hash as a hexadecimal string with a `0x` prefix.

**Returns:**

A `Transaction` object which contains the details of the transaction. The structure is based on `ITransactionsTransaction` from the consensus contracts. The object has the following fields:

- `id` (`string`): The transaction ID as a hex string.
- `txOrigin` (`address`): The original address that initiated the transaction.
- `sender` (`address`): The address of the sender.
- `recipient` (`address`): The destination address of the transaction.
- `activator` (`address`): The address of the validator that activated the transaction.
- `status` (`uint8`): The current status of the transaction.
- `statusName` (`string`): The ABI transaction status name for `status` (`Uninitialized`, `Pending`, `Proposing`, `Committing`, `Revealing`, `Accepted`, `Undetermined`, `Finalized`, `Canceled`, `AppealRevealing`, `AppealCommitting`, `ValidatorsTimeout`, `LeaderTimeout`, or `LeaderRevealing`).
- `previousStatus` (`null`): **Always null.** The contract dropped this field
  from `ITransactions.Transaction` (26 → 25 tuple components), so the node has
  nothing to report. It is `null` rather than `0` because `0` decodes as
  `Uninitialized`, which would be a wrong answer rather than an absent one. The
  field is removed in a v2 receipt.
- `txSlot` (`uint256`): The transaction slot number.
- `initialRotations` (`uint256`): The initial number of rotations.
- `numOfInitialValidators` (`uint256`): The number of initial validators.
- `randomSeed` (`string`): The random seed used for the transaction as a hex string.
- `txExecutionHash` (`string`): The execution hash of the transaction as a hex string.
- `txCallData` (`string`): The transaction call data as a hex string.
- `eqBlocksOutputs` (`string`): The equivalent blocks outputs as a hex string.
- `timestamps` (`object`): Object containing various timestamps:
  - `Created` (`uint256`): When the transaction was created.
  - `Pending` (`uint256`): When the transaction became pending.
  - `Activated` (`uint256`): When the transaction was activated.
  - `Proposed` (`uint256`): When the transaction was proposed.
  - `Committed` (`uint256`): When the transaction was committed.
  - `LastVote` (`uint256`): When the last vote was cast.
  - `AppealSubmitted` (`uint256`): When an appeal was submitted.
  - `LeaderRevealed` (`uint256`): When the leader was revealed.
- `result` (`uint8`): The result of the transaction execution.
- `txExecutionResult` (`uint8`): The leader execution vote result (`0` NotVoted, `1` FinishedWithReturn, `2` FinishedWithError, `3` Timeout, `4` NondetDisagree, `5` DeterministicViolation).
- `txExecutionResultName` (`string`): The ABI vote type name for `txExecutionResult`.
- `epoch` (`uint256`): The epoch the transaction belongs to.
- `resultHash` (`string`): The transaction result hash as a hex string.
- `validUntil` (`uint256`): The timestamp until which the transaction is valid.
- `fees` (`object|null`): Fee accounting echo. `null` when fee accounting is disabled or the FeeManager binding/address is not available. All numeric values in this object are decimal strings.
  - `deposit` (`string`): Submission-time fee custody sum, from `transactionFeesBudget(txId) + transactionDeveloperFeesBudget(txId)`.
  - `userValue` (`string`): User value carried by the transaction.
  - `distribution` (`object`): Effective fee distribution for the transaction:
    - `leaderTimeunitsAllocation` (`string`)
    - `validatorTimeunitsAllocation` (`string`)
    - `appealRounds` (`string`)
    - `executionBudgetPerRound` (`string`)
    - `totalMessageFees` (`string`)
    - `rotations` (`string[]`)
    - `maxPriceGenPerTimeUnit` (`string`)
    - `storageFeeMaxGasPrice` (`string`)
    - `receiptFeeMaxGasPrice` (`string`)
  - `locked` (`object|null`): Activation-locked prices, or `null` before activation when no locked prices exist:
    - `genPerTimeUnit` (`string`)
    - `storageUnitPrice` (`string`)
    - `receiptGasPrice` (`string`)
  - `consumed` (`object`): On-chain consumption counters:
    - `executionConsumed` (`string`)
    - `storageFeeUsed` (`string`): Storage fee charged on-chain for the transaction, equal to the storage pages it consumed multiplied by the locked `storageUnitPrice`.
    - `messageFeesConsumed` (`string`)
    - `messageFeesBudgetTotal` (`string`)
  - `hasAllocations` (`boolean`): `true` when the chain pinned the transaction's message allocations: a pinned per-message fee allocation tree, or a closed transaction, whose allowed set is empty. `false` for an open bucket (global message-fee bucket) or fee-less transactions. It is also `false` when the tree cannot be read, so that a receipt query still answers; the node logs the read failure.
  - `allocations` (`array`): The pinned per-message fee allocation tree, one entry per locked allocation, internal and external (empty for an open bucket and for a closed transaction). Budget comes from the on-chain allocation tree; `consumed` is read from the separate `allocationConsumed(txId, key)` mapping, keyed by `keccak256(abi.encode(messageType, recipient, callKey))`. Each entry:
    - `messageType` (`number`): `0` = external, `1` = internal.
    - `onAcceptance` (`boolean`): Emission phase the allocation is pinned to (`true` = on acceptance, `false` = on finalization).
    - `parentIndex` (`string`): Index of the parent allocation in the flat tree, or the root sentinel (`2^256 - 1`) for a root-layer allocation.
    - `recipient` (`string`): Target contract address the allocation funds. There is no recipient wildcard: the zero address is a concrete recipient.
    - `callKey` (`string`): 32-byte call key (hex) the allocation matches, or the `keccak256("")` call-key wildcard.
    - `budget` (`string`): Maximum message-fee budget locked for this allocation.
    - `consumed` (`string`): Amount consumed against this allocation so far.
    - `feeParams` (`string`): ABI-encoded per-message fee params (hex), decoded per `messageType`.
  - `allocationPolicy` (`string`, optional): How the chain classifies the transaction's message allocations. It is read from the chain's own policy, not derived from `hasAllocations` and `allocations`; read it to tell a closed transaction from the other two.

    | `allocationPolicy` | `hasAllocations` | `allocations` | Meaning |
    | --- | --- | --- | --- |
    | `"open"` | `false` | `[]` | Nothing is pinned. Messages draw from the open bucket. |
    | `"pinned"` | `true` | internal and external nodes | A message must match one of the listed allocations. |
    | `"closed"` | `true` | `[]` | Allocations are pinned to an empty allowed set. The transaction runs and can fund a message from its own contract balance; the chain rejects every other message it emits. |

    The field is omitted when the node could not read the allocation tree or the policy, or when the chain returns a policy this node version does not know; the node logs it. A receipt never states a policy it did not read.

    The values are read at the chain head. The chain deletes the allocation marker when it refunds a transaction's fee budget, so such a transaction reads `"open"` (and `hasAllocations: false`) whatever it was before. That applies to a cancelled transaction, to one that expired by idleness, and to one finalized through recovery; ordinary finalization keeps the policy. For those transactions `"open"` does not show that the transaction ran as an open bucket.
- `readStateBlockRanges` (`array`): Array of block range objects, each containing:
  - `ActivationBlock` (`uint256`): The activation block number.
  - `ProcessingBlock` (`uint256`): The processing block number.
  - `ProposalBlock` (`uint256`): The proposal block number.
- `roundData` (`array`): Array of round data objects containing:
  - `round` (`uint256`): The round number.
  - `leaderIndex` (`uint256`): Index of the round leader.
  - `votesCommitted` (`uint256`): Number of votes committed.
  - `votesRevealed` (`uint256`): Number of votes revealed.
  - `appealBond` (`uint256`): The appeal bond amount.
  - `rotationsLeft` (`uint256`): Number of rotations remaining.
  - `result` (`uint256`): The round result.
  - `roundValidators` (`array`): Array of validator addresses for this round.
  - `validatorVotes` (`string`): Base64-encoded validator votes.
  - `validatorVotesHash` (`array`): Array of vote hashes as hex strings.
  - `validatorResultHash` (`array`): Array of result hashes as hex strings.

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getTransactionReceipt",
  "params": [
    {
      "txId": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49"
    }
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49",
    "txOrigin": "0x5b70759760a85d92bf972b67ee8558e8e8da87d4",
    "sender": "0x5b70759760a85d92bf972b67ee8558e8e8da87d4",
    "recipient": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
    "activator": "0xf27b07d36ff5b9dcd37fd3a09811d6456dd4483a",
    "status": 13,
    "statusName": "LeaderRevealing",
    "previousStatus": null,
    "txSlot": 1,
    "initialRotations": 1,
    "numOfInitialValidators": 5,
    "randomSeed": "0x3d0bdff63bce1cf84b155f1557bb15e303b903b1b559325b6492794a5e498954",
    "txExecutionHash": "0x76744dc491b1725eb2ba08cf516855c14aac310e36711368c04442fd39d61853",
    "txCallData": "f5b31604617267730da4017570646174656420746573742073746f72616765066d6574686f64747570646174655f73746f7261676500",
    "eqBlocksOutputs": "c0",
    "timestamps": {
      "Created": 1750774777,
      "Pending": 1750774777,
      "Activated": 1750774781,
      "Proposed": 1750774786,
      "Committed": 1750774791,
      "LastVote": 0,
      "AppealSubmitted": 0,
      "LeaderRevealed": 0
    },
    "result": 0,
    "txExecutionResult": 3,
    "txExecutionResultName": "Timeout",
    "epoch": 12,
    "resultHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "validUntil": 0,
    "fees": {
      "deposit": "1234500000000000",
      "userValue": "0",
      "distribution": {
        "leaderTimeunitsAllocation": "100",
        "validatorTimeunitsAllocation": "200",
        "appealRounds": "0",
        "executionBudgetPerRound": "20000000000000000",
        "totalMessageFees": "0",
        "rotations": ["0"],
        "maxPriceGenPerTimeUnit": "1200000000000000",
        "storageFeeMaxGasPrice": "12",
        "receiptFeeMaxGasPrice": "300000000"
      },
      "locked": {
        "genPerTimeUnit": "0",
        "storageUnitPrice": "10",
        "receiptGasPrice": "250000000"
      },
      "consumed": {
        "executionConsumed": "0",
        "storageFeeUsed": "250",
        "messageFeesConsumed": "0",
        "messageFeesBudgetTotal": "0"
      },
      "hasAllocations": false,
      "allocations": [],
      "allocationPolicy": "open"
    },
    "readStateBlockRanges": [
      {
        "ActivationBlock": 1377797,
        "ProcessingBlock": 1377797,
        "ProposalBlock": 1377798
      }
    ],
    "roundData": [
      {
        "round": 0,
        "leaderIndex": 3,
        "votesCommitted": 5,
        "votesRevealed": 0,
        "appealBond": 0,
        "rotationsLeft": 1,
        "result": 0,
        "roundValidators": [
          "0xa707a1d9dd946242f921f0ad62f61e1b7df30c85",
          "0xf27b07d36ff5b9dcd37fd3a09811d6456dd4483a",
          "0xf3738216c3208d77a9db79a1d31824e5133083c4",
          "0x56394354538bff1e0ee5021c2b6d3522fce30257",
          "0xcfdbac032f82f388d9eca76efa6610b98e7f59fe"
        ],
        "validatorVotes": "AAAAAAA=",
        "validatorVotesHash": [
          "54e3478b02dfb009a35d46b19ac7c967b65416430a33d3860e9301a0f2b19d57",
          "f74a48e7283c91757bf2c612e40f8a9eb69ea0f02cf5e6b89c3c9b6ad629a3c1",
          "2f51c3b1d9ff35140260fb42330fbed12303a9915b8da786c9b8ed060983fa6c",
          "730127e27167fcf2450fe506250b85d916fbf07dd874b7f0c69a2f8ceb9e44a6",
          "473d57953f7e7c1a6fb1d2335618f21088025e02bd9fa30d6f85f20a55c24d15"
        ],
        "validatorResultHash": [
          "0000000000000000000000000000000000000000000000000000000000000000",
          "0000000000000000000000000000000000000000000000000000000000000000",
          "0000000000000000000000000000000000000000000000000000000000000000",
          "0000000000000000000000000000000000000000000000000000000000000000",
          "0000000000000000000000000000000000000000000000000000000000000000"
        ]
      }
    ]
  }
}
```

### gen_getTransactionStatus

Returns the transaction's current materialized consensus status. This is the lightweight polling surface: it does not project deadlines or turn protocol actions into synthetic statuses.

**Method:** `gen_getTransactionStatus`

**Parameters:**

- `request` (object, required):
  - `txId` (string, required): The transaction hash (hex-encoded with `0x` prefix)

**Returns:** Object with status string and numeric code

**Response Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `status` | string | Human-readable status name (e.g., "Finalized", "Accepted", "Pending") |
| `statusCode` | uint8 | Numeric status code |

**Status Codes:**

| Code | Status |
|------|--------|
| 0 | Uninitialized |
| 1 | Pending |
| 2 | Proposing |
| 3 | Committing |
| 4 | Revealing |
| 5 | Accepted |
| 6 | Undetermined |
| 7 | Finalized |
| 8 | Canceled |
| 9 | AppealRevealing |
| 10 | AppealCommitting |
| 11 | ValidatorsTimeout |
| 12 | LeaderTimeout |
| 13 | LeaderRevealing |

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getTransactionStatus",
  "params": [
    {
      "txId": "0x563f046c187d711127c51213ca62e2e4fee52009a98f0989a73a0a0382d21890"
    }
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "status": "Finalized",
    "statusCode": 7
  },
  "id": 1
}
```

**Notes:**

- This endpoint reads the stored status directly. A deadline passing does not change this response until the corresponding protocol transition is materialized.
- Use this for polling — it's cheaper than `gen_getTransactionReceipt` which returns the full receipt
- `Finalize` is an action, not a status. Explorers and operator tooling that need projected state or recovery actions should use `gen_getTransactionLifecycle`.

### gen_getTransactionLifecycle

Returns one advanced protocol lifecycle snapshot for a transaction. This endpoint is intended for explorers, operator tooling, and debugging; ordinary applications should prefer `gen_getTransactionStatus` or their SDK's simplified lifecycle model.

**Method:** `gen_getTransactionLifecycle`

**Parameters:**

- `request` (object, required):
  - `txId` (string, required): The transaction hash (hex-encoded with `0x` prefix)
  - `timestamp` (integer, optional): Unix timestamp used for projection. Defaults to the node clock.

**Returns:** The stored state, its projection at `evaluatedAt`, and the resolution kernel's next action from one contract read.

**Response Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `storedStatus` | string | Materialized on-chain status |
| `storedStatusCode` | uint8 | Materialized status code |
| `projectedStatus` | string | Status projected at `evaluatedAt`; projection does not write state |
| `projectedStatusCode` | uint8 | Projected status code |
| `resolutionAction` | string | Kernel action available from this snapshot, such as `NoOp`, `ResolveAppeal`, or `Finalize` |
| `resolutionActionCode` | uint8 | Resolution action code |
| `resolutionSource` | string | Resolution-plan source/reason |
| `resolutionSourceCode` | uint8 | Resolution source code |
| `decisionId` | string or null | Decimal decision identity used by decision-scoped protocol commands |
| `decisionActive` | boolean | Whether `decisionId` still identifies the active decision |
| `executionGeneration` | string or null | Current execution generation as a decimal string from the same snapshot; independent of retained decision provenance. Null when unavailable in a synthetic response. |
| `evaluatedAt` | integer | Unix timestamp used for the projection |

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getTransactionLifecycle",
  "params": [
    {
      "txId": "0x563f046c187d711127c51213ca62e2e4fee52009a98f0989a73a0a0382d21890",
      "timestamp": 1787868000
    }
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "storedStatus": "Accepted",
    "storedStatusCode": 5,
    "projectedStatus": "Accepted",
    "projectedStatusCode": 5,
    "resolutionAction": "Finalize",
    "resolutionActionCode": 6,
    "resolutionSource": "FullReveal",
    "resolutionSourceCode": 6,
    "decisionId": "42",
    "decisionActive": true,
    "executionGeneration": "3",
    "evaluatedAt": 1787868000
  },
  "id": 1
}
```

**Notes:**

- Every response field comes from one lifecycle read at one pinned head and one timestamp.
- `storedStatus` is the protocol state. `projectedStatus` describes what the kernel computes at `evaluatedAt`; it is not independently materialized.
- `resolutionAction: "Finalize"` is the authoritative finalization capability. There is deliberately no `ReadyToFinalize` status and no redundant `canFinalize` field.
- A client must not submit a decision-scoped command unless `decisionActive` is true and `decisionId` is non-null.

### gen_syncing

Returns the GenVM sync status, indicating how far the node's local GenVM state is behind the chain tip.

**Method:** `gen_syncing`

**Parameters:** None

**Returns:** Object with sync status information

**Response Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `syncedBlock` | string | Hex-encoded block number that GenVM has synced to |
| `latestBlock` | string | Hex-encoded latest block number on the chain |
| `blocksBehind` | number | Number of blocks GenVM is behind the chain tip (0 = fully synced) |

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_syncing",
  "params": [],
  "id": 1
}
```

**Example Response (Synced):**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "syncedBlock": "0x151ec5",
    "latestBlock": "0x151ec5",
    "blocksBehind": 0
  },
  "id": 1
}
```

**Example Response (Behind):**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "syncedBlock": "0x151e00",
    "latestBlock": "0x151ec5",
    "blocksBehind": 197
  },
  "id": 1
}
```

**Notes:**

- This endpoint always returns an object (never `false` like `eth_syncing`)
- Use `blocksBehind == 0` to determine if the node is fully synced
- When the node is catching up, `gen_call` requests without an explicit `blockNumber` will serve data at the `syncedBlock` rather than waiting for full sync

**cURL Example:**

```bash
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_syncing",
    "params": [],
    "id": 1
  }'
```

### gen_estimateGas

Reserved for estimating gas for a GenLayer transaction. **Not implemented** — the method is registered and reachable, but every call returns an error.

**Method:** `gen_estimateGas`

**Parameters:** None currently read — any params supplied are ignored because the method returns before parsing them

**Returns:** Nothing. The call always fails with JSON-RPC error code `-32004`

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_estimateGas",
  "params": [],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32004,
    "message": "method not implemented: gen_estimateGas"
  },
  "id": 1
}
```

**Notes:**

- The method is registered in the `genlayer` endpoint group, so it is reachable and rate-limited like any other method — it just cannot succeed
- The error code is `-32004` ("method not supported"), not `-32601` ("method not found"): the node recognises the method and is declining to serve it, rather than failing to route it
- Disabling the `genlayer` group, or this method specifically via `endpoints.methods`, changes the response to the same `-32004` code with a "method not supported" message
- Do not use gas estimation to size a transaction budget today; set fees explicitly instead

**cURL Example:**

```bash
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_estimateGas",
    "params": [],
    "id": 1
  }'
```

### gen_getLogs

Queries GenLayer contract-emitted events, mirroring `eth_getLogs` semantics under the `gen_` namespace. Events are produced by intelligent contracts (`gl.chain.Event` subclasses calling `.emit()`), persisted by the node while syncing accepted blocks, and indexed by contract address and topics.

**Method:** `gen_getLogs`

**Parameters:**

- `filter` (object, required):
  - `fromBlock` (`string`, optional): First block of the range (inclusive), as a hex (`0x…`), decimal string, or `"latest"`. Block numbers are rollup (L2) block numbers — the same numbering `gen_syncing` reports. Defaults to the latest GenVM-synced block.
  - `toBlock` (`string`, optional): Last block of the range (inclusive). Same format and default as `fromBlock`.
  - `address` (`string | string[]`, optional): Restrict to events emitted by this contract address (or any of these addresses). Empty matches all addresses.
  - `topics` (`array`, optional): Positional topic filters, at most 4 entries. Each position is `null` (wildcard), a 32-byte hex topic, or an array of topics (OR within the position). Positions match `eth_getLogs` semantics: an event matches when every constrained position matches; an event with fewer topics than a constrained position does not match.
  - `blockHash` (`string`, optional): 32-byte rollup block hash selecting exactly one block (EIP-234). Mutually exclusive with `fromBlock`/`toBlock`. The node resolves the hash through the rollup RPC; an unknown hash returns error `-32001`.
  - `status` (`string[]`, optional): Lifecycle stages to return, any of `"accepted"`, `"finalized"`, `"cancelled"`. Filters on the emitting transaction's **current** stage, not on the stage it held at `blockNumber`. Defaults to `["accepted", "finalized"]` — every event whose emitting round still stands. An unknown value returns error `-32602`.

**Returns:**

An array of log objects, ordered by block number, then transaction, then per-transaction event index:

- `address` (`string`): Address of the emitting contract.
- `topics` (`string[]`): The emitted topics, each a 32-byte hex string. `topics[0]` is the event signature topic: `keccak256` of the UTF-8 string `EventName(sortedIndexedFieldNames)` — GenLayer hashes the **sorted indexed field names without types**, unlike Solidity's typed signature. Remaining topics are the indexed field values (calldata-encoded, right-padded to 32 bytes, or keccak-hashed when longer).
- `data` (`string`): Hex of the event payload. **Deviation from eth:** this is the GenVM **calldata** encoding of the event's keyword-argument map, not ABI encoding — decode it with a GenLayer SDK calldata codec.
- `blockNumber` (`string`): Rollup block number (hex) of the block whose sync recorded the event.
- `transactionHash` (`string`): 32-byte GenLayer transaction ID.
- `logIndex` (`string`): **Deviation from eth:** index of the event **within its transaction** (hex), not within the block.
- `round` (`number`): GenLayer extra — the consensus round of the executed result that emitted the event.
- `status` (`string`): GenLayer extra — the emitting transaction's current lifecycle stage: `"accepted"`, `"finalized"`, or `"cancelled"`. **Not** the `gen_getTransactionStatus` vocabulary: that method's `"Canceled"` means a transaction cancelled before execution, whereas a `"cancelled"` log belongs to a transaction whose acceptance an appeal overturned and which is re-executed on a later round.
- `removed` (`boolean`): Carries eth's meaning — the log is no longer part of the canonical set. `true` exactly when `status` is `"cancelled"`.

`blockHash` and `transactionIndex` are omitted (not indexed node-side).

**Semantics and limitations:**

- GenLayer events follow their transaction's lifecycle: they become emitted when the transaction is **ACCEPTED**, become **FINALIZED** with it, and can be **CANCELLED** by a successful appeal. Events from **successful, accepted** executions are recorded at acceptance time (a reverted transaction emits no logs, matching EVM semantics).
- **A log belongs to the block that emitted it.** `blockNumber` is always the acceptance block and `fromBlock`/`toBlock` select on it, so a log's block is stable while its `status` moves. Each log is returned exactly once per query, whatever combination of statuses is requested — including when a transaction is accepted and finalized within the same block.
- **`status` is current, not as-of.** The same query run twice can report a different `status` for a log (accepted → finalized) or stop returning it (accepted → cancelled), the same way `gen_getTransactionStatus` reports current state.
- **Default excludes cancelled logs.** A query with no `status` returns `["accepted", "finalized"]`. Events whose round was overturned on appeal are reachable with `status: ["cancelled"]`, where they carry `removed: true`.
- **Retention.** Operators bound the store with `node.events.retention_blocks` (default 1,000,000 blocks; `0` disables). Queries below the retention watermark return no logs.
- **Deviation from eth:** an event emitted inside a write-enabled sandbox (`allow_write_ops=True`) whose failure the contract catches still surfaces on the successful transaction — GenVM sandboxes are permission boundaries, not transactional ones, so caught-failure branch events are part of the transaction's log set (EVM drops reverted-sub-call logs).
- Coverage requires the transaction to have executed on an executor that reports events in its result (all executors bundled with the currently pinned GenVM manager do); a future pin bundling a pre-2025-12 executor would not surface events for transactions it executes.
- Results reflect **synced** state: a transaction accepted moments ago becomes queryable once the sync loop writes its block (same trailing semantics as `gen_getContractState` at `latest`).

**Limits:**

| Limit | Default | Config key |
|---|---|---|
| Block range per query | 10,000 blocks | `node.rpc.getlogs_max_block_range` |
| Logs per response | 10,000 entries | `node.rpc.getlogs_max_results` |
| Event retention | 1,000,000 blocks | `node.events.retention_blocks` |

Both are node-configurable (operators may reduce them); exceeding either returns error `-32005` (`query exceeds limits: …` — distinct from HTTP rate limiting, which uses the same code). The method carries a default rate limit of 2 requests per second per IP, also configurable.

**Errors:**

- `-32602`: malformed filter (invalid hex, more than 4 topic positions, non-32-byte topic, `blockHash` combined with a block range, `fromBlock` greater than `toBlock`, unknown `status` value).
- `-32005`: block range or result count over the configured cap.
- `-32001`: `blockHash` unknown to the rollup.
- `-32000`: internal failure (details are logged, not returned).

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getLogs",
  "params": [
    {
      "fromBlock": "0x14e300",
      "toBlock": "0x14e400",
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "topics": [
        "0x9f6dbb5f0f942152b8b254e69610e1c65e33a1bb61bd8f4bd88f0ec7bbe15c2f",
        null
      ]
    }
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "address": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
      "topics": [
        "0x9f6dbb5f0f942152b8b254e69610e1c65e33a1bb61bd8f4bd88f0ec7bbe15c2f",
        "0x2a00000000000000000000000000000000000000000000000000000000000000"
      ],
      "data": "0x261674696d657374616d70014996d2c96e",
      "blockNumber": "0x14e309",
      "transactionHash": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49",
      "logIndex": "0x0",
      "round": 0,
      "status": "finalized",
      "removed": false
    }
  ]
}
```

### gen_getTransactionData

Retrieves the on-chain transaction data for one round together with the contract execution outputs (`returnData`, `leaderPublicData`) recovered from the node's local trace store. If the node no longer holds the trace (restart, pruning), the on-chain fields are still returned with `returnData: null` — the transaction is never replayed.

**Method:** `gen_getTransactionData`

**Parameters:**

- `request` (object, required):
  - `txId` (`string`, required): The 32-byte transaction hash as a hexadecimal string with a `0x` prefix.
  - `round` (`uint8`, optional): The consensus round whose execution outputs to return. Defaults to the latest round.

**Returns:**

An object with the on-chain transaction fields for the selected round plus the locally stored execution outputs:

- `id` (`string`): The transaction ID as a hex string.
- `sender` (`address`): The address of the sender.
- `recipient` (`address`): The destination address of the transaction.
- `activator` (`address`): The address of the validator that activated the transaction.
- `status` (`uint8`): The current status of the transaction.
- `statusName` (`string`): The ABI transaction status name for `status` (`Uninitialized`, `Pending`, `Proposing`, `Committing`, `Revealing`, `Accepted`, `Undetermined`, `Finalized`, `Canceled`, `AppealRevealing`, `AppealCommitting`, `ValidatorsTimeout`, `LeaderTimeout`, or `LeaderRevealing`).
- `result` (`uint8`): The consensus result of the transaction (`0` Idle, `1` MajorityAgree, `2` MajorityDisagree, `3` MajorityTimeout, `4` DeterministicViolation, `5` NoMajority).
- `resultName` (`string`): The ABI result name for `result`.
- `txExecutionResult` (`uint8`): The leader execution vote result (`0` NotVoted, `1` FinishedWithReturn, `2` FinishedWithError, `3` Timeout, `4` NondetDisagree, `5` DeterministicViolation).
- `txExecutionResultName` (`string`): The ABI vote type name for `txExecutionResult`.
- `txCallData` (`string`): The transaction call data as a hex string.
- `txExecutionHash` (`string`): The execution hash of the transaction as a hex string.
- `eqBlocksOutputs` (`string`): The equivalent blocks outputs as a hex string.
- `timestamps` (`object`): Object containing various timestamps:
  - `Created` (`uint256`): When the transaction was created.
  - `Pending` (`uint256`): When the transaction became pending.
  - `Activated` (`uint256`): When the transaction was activated.
  - `Proposed` (`uint256`): When the transaction was proposed.
  - `Committed` (`uint256`): When the transaction was committed.
  - `LastVote` (`uint256`): When the last vote was cast.
  - `AppealSubmitted` (`uint256`): When an appeal was submitted.
  - `LeaderRevealed` (`uint256`): When the leader was revealed.
- `round` (`uint8`): The round the execution outputs belong to (the requested round, or the latest round when omitted).
- `returnData` (`string|null`): The contract's return data as a `0x`-prefixed hex string, from the node's locally stored execution trace. `null` when the node holds no trace for the round — clients must handle `null` explicitly.
- `leaderPublicData` (`string`): Leader data as a `0x`-prefixed hex string from the stored trace. Its encoding is defined by the GenVM specification. `0x` when no trace is available.

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_getTransactionData",
  "params": [
    {
      "txId": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49"
    }
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49",
    "sender": "0x5b70759760a85d92bf972b67ee8558e8e8da87d4",
    "recipient": "0x73ca5a2b51edf506ceaf110a41780ec51294d89f",
    "activator": "0xf27b07d36ff5b9dcd37fd3a09811d6456dd4483a",
    "status": 7,
    "statusName": "Finalized",
    "result": 1,
    "resultName": "MajorityAgree",
    "txExecutionResult": 1,
    "txExecutionResultName": "FinishedWithReturn",
    "txCallData": "f5b31604617267730da4017570646174656420746573742073746f72616765066d6574686f64747570646174655f73746f7261676500",
    "txExecutionHash": "0x76744dc491b1725eb2ba08cf516855c14aac310e36711368c04442fd39d61853",
    "eqBlocksOutputs": "c0",
    "timestamps": {
      "Created": 1750774777,
      "Pending": 1750774777,
      "Activated": 1750774781,
      "Proposed": 1750774786,
      "Committed": 1750774791,
      "LastVote": 1750774795,
      "AppealSubmitted": 0,
      "LeaderRevealed": 0
    },
    "round": 0,
    "returnData": "0x00",
    "leaderPublicData": "0x0e076e645f6f7574730d1b01aabb"
  }
}
```

**Notes:**

- Multi-round appeal data (`roundData`) and fee accounting stay on `gen_getTransactionReceipt`; debug output (stdout, stderr, genvm_log) and trace replay stay on `gen_dbg_traceTransaction`.
- `returnData` and `leaderPublicData` come from the node's local trace store only — a node that never executed the transaction (or pruned the trace) serves `returnData: null` and `leaderPublicData: "0x"` without replaying.
- **Retention.** `returnData` and `leaderPublicData` come from the trace store, which operators bound with `node.trace.retention_blocks` (default 1,000,000 blocks; `0` disables). Past the watermark this endpoint answers from chain data with `returnData: null`, exactly as it does for a transaction the node never executed.
- The trace is persisted asynchronously after a transaction is accepted, so a query issued immediately after acceptance can still return `returnData: null`. Clients that need the execution output should poll until it appears (or fall back to `gen_dbg_traceTransaction`, which replays on demand).
- An explicit `round` beyond the rounds the transaction has returns a `-32001` error.
- A malformed `txId` (not a 32-byte hex string) returns `-32602`; a transaction not found on-chain returns `-32001`.
- This endpoint is in the `genlayer` group and is enabled by default.

**cURL Example:**

```bash
# Latest round
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getTransactionData",
    "params": [{
      "txId": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49"
    }],
    "id": 1
  }'

# Specific round
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getTransactionData",
    "params": [{
      "txId": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49",
      "round": 1
    }],
    "id": 1
  }'
```

### gen_protocolVersion

Reserved for reporting the GenLayer protocol version. **Not implemented** — the method is registered and reachable, but every call returns an error.

**Method:** `gen_protocolVersion`

**Parameters:** None currently read — any params supplied are ignored because the method returns before parsing them

**Returns:** Nothing. The call always fails with JSON-RPC error code `-32004`

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_protocolVersion",
  "params": [],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32004,
    "message": "method not implemented: gen_protocolVersion"
  },
  "id": 1
}
```

**Notes:**

- The method is registered in the `genlayer` endpoint group, so it is reachable and rate-limited like any other method — it just cannot succeed
- The error code is `-32004` ("method not supported"), not `-32601` ("method not found"): the node recognises the method and is declining to serve it, rather than failing to route it
- To identify what a node is running, use the ops server's `GET /health`, which reports node version and sync state
- `eth_protocolVersion` is unaffected — it is proxied to the GenLayer chain like any other `eth_*` method

**cURL Example:**

```bash
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_protocolVersion",
    "params": [],
    "id": 1
  }'
```

### gen_subscribe

Opens a subscription to a stream of GenLayer consensus events over the node's inbound WebSocket endpoint. Notifications are pushed as they occur, removing the need to poll `gen_getTransactionStatus`.

**Method:** `gen_subscribe`

**Transport:** WebSocket only. This method is not available over HTTP — an HTTP POST cannot receive push notifications. See the WebSocket endpoint section for the URL.

**Parameters:**

- `stream` (string, required): The event stream to subscribe to. One of `transactions`, `leadership`, `epochs`, `newHeads`.

**Returns:** A subscription id (UUID v4 string) used to correlate notifications and to cancel via `gen_unsubscribe`

**Streams:**

| Stream | Event kinds delivered |
|--------|-----------------------|
| `transactions` | `NewTransaction`, `TransactionActivated`, `TransactionReceiptProposed`, `VoteCommitted`, `VoteRevealed`, `TransactionAccepted`, `TransactionFinalized`, `TransactionUndetermined` |
| `leadership` | `TransactionLeaderRotated`, `TransactionLeaderTimeout`, `TransactionLeaderChanged`, `TransactionLeaderRevealed`, `LeaderIdlenessProcessed`, `AppealStarted` |
| `epochs` | `EpochAdvance`, `EpochFinalize`, `TimeoutsSet`, `SetEpochMinDuration` |
| `newHeads` | `NewHeader` |

**Notification Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `kind` | string | The event name, matching the consensus contract's ABI event (e.g. `TransactionAccepted`) |
| `event` | object | The decoded event payload. `bytes32` values are 0x-prefixed hex; field names are camelCase |
| `event.raw` | object | The originating chain log (block hash, transaction hash, topics), for reconciling against `gen_getLogs` |

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_subscribe",
  "params": [
    "transactions"
  ],
  "id": 1
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "result": "b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33",
  "id": 1
}
```

**Example Notification:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_subscription",
  "params": {
    "subscription": "b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33",
    "result": {
      "kind": "TransactionAccepted",
      "event": {
        "txId": "0x9c1f3a7b8d2e4f5061728394a5b6c7d8e9f0a1b2c3d4e5f60718293a4b5c6d7e",
        "raw": {
          "address": "0x1234567890abcdef1234567890abcdef12345678",
          "blockNumber": "0x151ec5",
          "transactionHash": "0x4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c"
        }
      }
    }
  }
}
```

**Notes:**

- The subscription id is a UUID v4 generated by this node; it is never an upstream chain subscription id
- Streams are coarse by design: one subscription covers a whole lifecycle, so following a transaction end to end costs one subscription rather than one per event kind
- The `kind` values are the consensus contracts' ABI event names, which are stable across node releases. They are deliberately not the node's internal topic names
- Every payload is derived from on-chain logs and contains no node-private data. The node's own GenVM sync position is not exposed here; use `gen_syncing`
- `newHeads` is served for convenience and duplicates `eth_subscribe("newHeads")`, which is fed from the same upstream. Latency-sensitive clients should prefer `eth_subscribe`
- Notifications are live only — there is no historical replay or backfill. Reconcile any gap with `gen_getLogs`
- Server-side filtering (for example by `txId`) is not supported; subscribe to the stream and filter client-side
- Subscribing to an unknown stream returns error code `-32602` with the list of supported streams
- Exceeding `max_subs_per_conn` returns error code `-32000` ("subscription limit reached")
- This method belongs to the `genlayer` endpoint group; disabling that group disables it on the WebSocket transport too
- Standard clients can consume this with their generic subscription API, since notifications use the conventional `gen_subscription` envelope. In go-ethereum: `client.Subscribe(ctx, "gen", ch, "transactions")`

**wscat Example:**

```bash
wscat -c ws://localhost:9151/ws
> {"jsonrpc":"2.0","method":"gen_subscribe","params":["transactions"],"id":1}
< {"jsonrpc":"2.0","result":"b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33","id":1}
< {"jsonrpc":"2.0","method":"gen_subscription","params":{"subscription":"b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33","result":{"kind":"NewTransaction","event":{"txId":"0x9c1f..."}}}}
```

### gen_unsubscribe

Cancels a subscription previously opened with `gen_subscribe`. The node stops sending notifications for that subscription and releases the resources backing it.

**Method:** `gen_unsubscribe`

**Transport:** WebSocket only, on the same connection that opened the subscription.

**Parameters:**

- `subscriptionId` (string, required): The id returned by `gen_subscribe`

**Returns:** Boolean — `true` when a matching subscription was cancelled, `false` when the id is unknown on this connection

**Example Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_unsubscribe",
  "params": [
    "b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33"
  ],
  "id": 2
}
```

**Example Response:**

```json
{
  "jsonrpc": "2.0",
  "result": true,
  "id": 2
}
```

**Notes:**

- An unknown subscription id is not an error: the response is `result: false`, matching the `eth_unsubscribe` convention
- Subscriptions are scoped to the connection that created them. One connection cannot cancel another's subscription, and an id from a closed connection always returns `false`
- Explicit unsubscription is optional for cleanup: closing the WebSocket cancels every subscription on that connection automatically
- A notification already in flight when the cancellation is processed may still arrive; treat the `true` response as "no further events will be produced", not as a barrier
- A missing or non-string parameter returns error code `-32602`

**wscat Example:**

```bash
wscat -c ws://localhost:9151/ws
> {"jsonrpc":"2.0","method":"gen_unsubscribe","params":["b3f1c2de-9a44-4c1e-8f77-2b0a5d6e1c33"],"id":2}
< {"jsonrpc":"2.0","result":true,"id":2}
```

## Debug Methods

These methods are available for debugging and testing purposes during development.

Debug methods are off by default. To enable them, set `genlayer_debug.enabled: true` under `node.rpc.endpoints` in `config.yaml`. Enable them only on an RPC port that untrusted callers cannot reach:

```yaml
node:
  rpc:
    endpoints:
      genlayer_debug:
        enabled: true
```

### gen_dbg_ping

Simple connectivity test method that returns a "pong" response.

**Method:** `gen_dbg_ping`

**Parameters:** None

**Returns:** "pong" string

**Example:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_dbg_ping",
  "params": [],
  "id": 1
}
```

**Response:**

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

### gen_dbg_trie

Get trie information for debugging node internal DB state.

**Method:** `gen_dbg_trie`

**Parameters:**

- `request` (object, required): The trie debug request
  - `txID` (string, required): Transaction ID (hex-encoded)
  - `round` (integer, required): Round number

**Returns:** Trie debug information string

**Example:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_dbg_trie",
  "params": [
    {
      "txID": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6742d35Cc6634C0532925a3b8",
      "round": 0
    }
  ],
  "id": 1
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "result": "graph TD\nnode0@{label: \"br  seq=1\"}\nnode1@{label: \"ext seq=1 0x00000000000000000000000000000000000000000000000000000000000000000000000\"}\nnode1 --\u003e data2@{label: \"seq=1\n[21 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]\"}\nnode0 --\u003e|0| node1\nnode3@{label: \"ext seq=1 0x72d46c3ada9f897c74d349bbfe0e450c798167c9f580f8daf85def57e96c3ea00000000\"}\nnode3 --\u003e data4@{label: \"seq=1\n[105 110 105 116 105 97 108 32 115 116 111 114 97 103 101 32 118 97 108 117 101 0 0 0 0 0 0 0 0 0 0 0]\"}\nnode0 --\u003e|3| node3\n",
  "id": 1
}
```

### gen_dbg_traceTransaction

Retrieves the full execution trace for a transaction, including the return data, stdout/stderr, GenVM logs, and the exact error if execution failed.

**Method:** `gen_dbg_traceTransaction`

**Parameters:**

- `request` (object, required):
  - `txID` (string, required): Transaction ID (hex-encoded with `0x` prefix)
  - `round` (integer, optional): Consensus round number (default: 0)

**Returns:** Transaction execution trace object

**Response Fields:**

| Field | Type | Description |
|-------|------|-------------|
| `transaction_id` | string | Transaction hash |
| `result_code` | number | 0=success, 1=user error, 2=VM error, 3=internal error |
| `return_data` | string | Hex-encoded contract return data |
| `stdout` | string | Standard output captured during execution |
| `stderr` | string | Standard error captured during execution |
| `genvm_log` | array | Detailed GenVM execution log entries |
| `storage_proof` | string | Hex-encoded storage proof hash |
| `run_time` | string | Execution duration |
| `leader_public_data` | string | Hex-encoded leader data; its encoding is defined by the GenVM specification |
| `stored_at` | string (optional) | ISO 8601 timestamp of when the trace was stored; omitted when not available |

**Example:**

```json
{
  "jsonrpc": "2.0",
  "method": "gen_dbg_traceTransaction",
  "params": [{"txID": "0x563f046c187d711127c51213ca62e2e4fee52009a98f0989a73a0a0382d21890", "round": 0}],
  "id": 1
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "transaction_id": "0x563f046c187d711127c51213ca62e2e4fee52009a98f0989a73a0a0382d21890",
    "result_code": 2,
    "return_data": "0x2e0464617461b402696e76616c69645f636f6e7472616374...",
    "stdout": "",
    "stderr": "",
    "genvm_log": [
      {
        "file": "src/lib.rs:82",
        "level": "error",
        "message": "vm error",
        "error": {"causes": ["VMError(invalid_contract absent_runner_comment)"]}
      }
    ],
    "storage_proof": "0x99ff0d9125e1fc9531a11262e15aeb2c60509a078c4cc4c64cefdfb06ff68647",
    "run_time": "0s",
    "leader_public_data": "0x0e076e645f6f75747305"
  },
  "id": 1
}
```

**Notes:**

- If the trace isn't stored locally (e.g., the node restarted or data was pruned), the endpoint will replay the transaction automatically.
- **Retention.** Operators bound the trace store with `node.trace.retention_blocks` (default 1,000,000 blocks; `0` disables). A trace below the retention watermark is gone, so this endpoint falls back to a live replay — reconstructed from the latest on-chain view, and not byte-identical to the stored artifact. A single trace larger than `node.trace.max_trace_bytes` (default 32 MB) is never stored, which produces the same fallback.
- **Field bound.** The free-form execution-output fields (`return_data`, `stdout`, `stderr`, `genvm_log`) carry whatever the replayed contract emitted, so their size is contract-controlled. Each is capped at `node.trace.max_debug_field_bytes` (default 256 KB; `0` takes the default). When a field is cut, the truncation is marked in-band — a trailing `...[truncated: N of M bytes omitted from gen_dbg trace]` on a string field, or a final `{"truncated": "..."}` entry on `genvm_log` — so the reply stays honest rather than silently short.
- This endpoint is in the `genlayer_debug` group, which is **disabled by default**. Enable it deliberately, and only on an RPC bind that untrusted callers cannot reach: it replays a transaction through GenVM and returns internal execution output, so it is debug tooling, not a public API.
- Use `result_code` to determine execution outcome: 0 = contract returned successfully, 1 = contract raised a user error, 2 = GenVM encountered an error, 3 = internal error.

## Ethereum Compatibility

The GenLayer Node also supports Ethereum-compatible methods that are proxied to the underlying infrastructure. These methods follow the standard [Ethereum JSON-RPC specification](https://ethereum.org/en/developers/docs/apis/json-rpc/) and are prefixed with `eth_`.

**Examples of supported Ethereum methods:**

- `eth_blockNumber`
- `eth_getBalance`
- `eth_sendRawTransaction`
- `eth_call`
- And other standard Ethereum JSON-RPC methods

## zkSync Compatibility

[zkSync-compatible](https://docs.zksync.io/zksync-protocol/api/zks-rpc) methods are also supported and proxied to the underlying infrastructure. These methods are prefixed with `zks_`.

The default `config.yaml` turns the `zksync` group off (`node.rpc.endpoints.zksync.enabled: false`). Set it to `true` to forward `zks_*` calls.

## Usage Examples

### cURL

```bash
# Test connectivity (debug method, needs genlayer_debug enabled)
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_dbg_ping",
    "params": [],
    "id": 1
  }'

# Execute a contract call
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_call",
    "params": [{
      "from": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6",
      "to": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6",
      "data": "0x70a08231000000000000000000000000742d35cc6634c0532925a3b8d4c9db96c4b4d8b6",
      "type": "read",
      "status": "decided"
    }],
    "id": 1
  }'

# Get contract schema
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getContractSchema",
    "params": [{
      "code": "IyB7ICJEZXBlbmRzIjogInB5LWdlbmxheWVyOnRlc3QiIH0KCmZyb20gZ2VubGF5ZXIgaW1wb3J0ICoKCgojIGNvbnRyYWN0IGNsYXNzCmNsYXNzIFN0b3JhZ2UoZ2wuQ29udHJhY3QpOgogICAgc3RvcmFnZTogc3RyCgogICAgIyBjb25zdHJ1Y3RvcgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGluaXRpYWxfc3RvcmFnZTogc3RyKToKICAgICAgICBzZWxmLnN0b3JhZ2UgPSBpbml0aWFsX3N0b3JhZ2UKCiAgICAjIHJlYWQgbWV0aG9kcyBtdXN0IGJlIGFubm90YXRlZCB3aXRoIHZpZXcKICAgIEBnbC5wdWJsaWMudmlldwogICAgZGVmIGdldF9zdG9yYWdlKHNlbGYpIC0+IHN0cjoKICAgICAgICByZXR1cm4gc2VsZi5zdG9yYWdlCgogICAgIyB3cml0ZSBtZXRob2QKICAgIEBnbC5wdWJsaWMud3JpdGUKICAgIGRlZiB1cGRhdGVfc3RvcmFnZShzZWxmLCBuZXdfc3RvcmFnZTogc3RyKSAtPiBOb25lOgogICAgICAgIHNlbGYuc3RvcmFnZSA9IG5ld19zdG9yYWdlCg=="
    }],
    "id": 1
  }'

# Get debug trie information (debug method, needs genlayer_debug enabled)
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_dbg_trie",
    "params": [{
      "txID": "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8b6742d35Cc6634C0532925a3b8",
      "round": 0
    }],
    "id": 1
  }'

# Get transaction receipt
curl -X POST http://localhost:9151 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "gen_getTransactionReceipt",
    "params": [{
      "txId": "0x635060dd514082096d18c8eb64682cc6a944f9ce1ae6982febf7a71e9f656f49"
    }],
    "id": 1
  }'
```

## Ops Methods

These methods provide operational endpoints for monitoring the GenLayer node.

### balance

Returns the configured operator's account balance in multiple formats including hexadecimal, wei, and GEN units.

**Method:** `GET /balance`

**Parameters:** None

**Returns:** JSON object containing the operator's balance in three formats

**Example Request:**

```bash
curl -X GET http://localhost:9153/balance
```

**Example Response:**

```json
{
  "balance_hex": "0x0de0b6b3a7640000",
  "balance_wei": "1000000000000000000",
  "balance_gen": "1.000"
}
```

**Response Fields:**

- `balance_hex` (string): The balance encoded as hexadecimal with "0x" prefix
- `balance_wei` (string): The balance in wei as a decimal string
- `balance_gen` (string): The balance in GEN units formatted with 3 decimal places

**Notes:**

- This endpoint is only available when the node is running in validator mode
- The balance is always retrieved from the latest block
- The operator address is configured at node startup
- If the operator address is not configured, the zero address (0x0000...0000) is used
- Balance conversions: 1 GEN = 1,000,000,000,000,000,000 wei (10^18 wei)
- Returns HTTP 503 if the node is not in validator mode
- Returns HTTP 500 if there's an error retrieving the balance

**Example Response (Zero Balance):**

```json
{
  "balance_hex": "0x",
  "balance_wei": "0",
  "balance_gen": "0.000"
}
```

**Example Response (Large Balance):**

```json
{
  "balance_hex": "0x3635c9adc5dea00000",
  "balance_wei": "1000000000000000000000",
  "balance_gen": "1000.000"
}
```

**Error Response (Not in Validator Mode):**

```json
{
  "message": "Balance endpoint not available in full mode"
}
```

**cURL Example with Headers:**

```bash
curl -X GET http://localhost:9153/balance \
  -H "Accept: application/json" \
  -v
```

### health

Returns the node's operational status and the typed condition catalog that explains it.

**Method:** `GET /health`

**Parameters:** None

**Returns:** JSON object containing the overall status, network/version information, validator identity (validator mode), and the full condition catalog

**HTTP status:** `503` when `status` is `unavailable`; `200` for every other state.

**Example Request:**

```bash
curl -X GET http://localhost:9153/health
```

**Example Response:**

```json
{
  "status": "healthy",
  "network": "",
  "node_version": "v0.6.0",
  "protocol_version": "0.3.0",
  "genvm_version": "v0.0.18",
  "validator_address": "0x1234567890abcdef1234567890abcdef12345678",
  "validator_moniker": "my-validator",
  "conditions": {
    "chain.rpc":                { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "chain.websocket":          { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "chain.sync":               { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:02:14Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "consensus.ban":            { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "consensus.stake":          { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "consensus.operator_funds": { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "consensus.prime":          { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:04:41Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "genvm.llm":                { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:31Z", "updated_at": "2026-09-01T10:29:56Z", "last_ok": "2026-09-01T10:29:56Z" },
    "genvm.web":                { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:31Z", "updated_at": "2026-09-01T10:29:56Z", "last_ok": "2026-09-01T10:29:56Z" },
    "node.disk":                { "state": "healthy", "severity": "critical", "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" },
    "node.snapshot_age":        { "state": "healthy", "severity": "info",     "occurrences": 0, "consecutive_failures": 0, "since": "2026-09-01T08:00:03Z", "updated_at": "2026-09-01T10:31:12Z", "last_ok": "2026-09-01T10:31:12Z" }
  }
}
```

**Response Fields:**

- `status` (string): the most urgent state across the catalog — `unknown`, `healthy`, `suspect`, `warning`, `degraded`, or `unavailable`
- `network` (string): The network the node is connected to
- `node_version`, `protocol_version`, `genvm_version` (string): build versions
- `validator_address` (string, validator mode only): The validator's address
- `validator_moniker` (string, validator mode only): The validator's on-chain moniker; reported alongside the address even when empty
- `conditions` (object): the full catalog. Every entry is always present — this is not a list of what is broken.
  - `state` (string): current state
  - `severity` (string): the catalog's fixed class — `info`, `warning`, or `critical`. Not the state: a `critical` condition can be `healthy`.
  - `occurrences` (number): accepted failed observations since process start
  - `consecutive_failures` (number): current failure streak; zero after recovery
  - `since`, `updated_at`, `last_ok` (string, optional): ISO 8601 lifecycle timestamps
  - `reason` (string, present only when not healthy): stable, low-cardinality cause code
  - `impact` (string, present only when not healthy): what the fault does to this node
  - `action` (string, present only when not healthy): what the operator should do
  - `details` (object, present only when not healthy): typed fields explicitly allowlisted by the catalog entry. Raw provider payloads, prompts, credentials, URLs, and stack traces are rejected.

**States**

| State | Meaning | Alerted |
|---|---|---|
| `unknown` | The node never observed this condition. | no |
| `healthy` | The last observation succeeded. | no |
| `suspect` | One failure, not yet confirmed. It may clear by itself. | no |
| `warning` | The capability works, and it is confirmed sub-optimal. | yes |
| `degraded` | The failure is confirmed. The capability does not work. | yes |
| `unavailable` | The node cannot do its duty at all. | yes |

Roll-up precedence, most urgent first: `unavailable`, `degraded`, `warning`, `suspect`, `unknown`, `healthy`.

`unknown` is not on that ladder. It is only ever the state a condition starts in, so it means "not observed yet", never "faulty". A GenVM module rests there until a transaction needs one; `node.snapshot_age` rests there on a node that takes no snapshots. Letting either gate the endpoint would report `unknown` for a node where nothing is wrong.

If **no** condition has been observed at all, `status` is `unknown` — nothing observed is not the same as nothing wrong.

**Only `unavailable` returns 503.**

A condition can never exceed the liveness cap its catalog entry declares. The GenVM conditions are capped at `degraded`, so a dead LLM provider returns **200**: that node is still booted, synced and voting — it votes Timeout on the transactions it cannot execute, which is the designed behaviour rather than an outage. Failing the endpoint would pull a working validator out of a load balancer.

`consensus.ban`, `consensus.stake`, `chain.rpc`, `chain.websocket` and `chain.sync` are capped at `unavailable`, because each of them stops the node doing its job.

**Example: the LLM provider is dead — HTTP 200**

```json
{
  "status": "degraded",
  "conditions": {
    "genvm.llm": {
      "state": "degraded",
      "severity": "critical",
      "reason": "llm_request_failed",
      "impact": "This validator cannot execute a transaction that needs an LLM. It votes Timeout, and the consensus contracts may penalize a Timeout vote.",
      "action": "Check the affected LLM provider service and payment or billing status, restore access, or configure a working fallback. If credentials are changed through .env, restart the node.",
      "occurrences": 2,
      "consecutive_failures": 2,
      "since": "2026-09-01T10:30:45Z",
      "updated_at": "2026-09-01T10:31:12Z",
      "last_ok": "2026-09-01T04:12:03Z"
    }
  }
}
```

**Example: the validator is banned — HTTP 503**

```json
{
  "status": "unavailable",
  "conditions": {
    "consensus.ban": {
      "state": "unavailable",
      "severity": "critical",
      "reason": "validator_banned",
      "impact": "The consensus contracts do not select this validator for a round. The node earns nothing.",
      "action": "Wait for the stated epoch. If the ban is permanent, restake with a new validator.",
      "details": { "until_epoch": "4182", "permanent": "false" },
      "occurrences": 1,
      "consecutive_failures": 1,
      "since": "2026-09-01T09:14:02Z",
      "updated_at": "2026-09-01T10:31:12Z",
      "last_ok": "2026-09-01T09:13:52Z"
    }
  }
}
```

**`chain.sync` — 503 only when the node stops syncing**

Depth is not a fault. A node syncing from genesis is millions of blocks behind and perfectly healthy. What matters is whether the stored synced block advances.

| Reason | State | HTTP |
|---|---|---|
| `sync_lagging` | `warning` | 200 |
| `sync_catching_up` | `degraded` | 200 |
| `sync_stalled` | `unavailable` | **503** |
| `sync_feed_frozen` | `unavailable` | **503** |

The node never skips a duty because it is behind. It votes Timeout only on a transaction that reads state it has not synced; a new contract deploy still runs.

**cURL Example with Headers:**

```bash
curl -X GET http://localhost:9153/health \
  -H "Accept: application/json" \
  -v
```

### metrics

Exposes Prometheus-compatible metrics for monitoring node performance and health.

**Method:** `GET /metrics`

**Parameters:** None

**Returns:** Prometheus-format text response with all collected metrics

**Example Request:**

```bash
curl -X GET http://localhost:9153/metrics
```

**Example Response:**

```
# HELP genlayer_node_cpu_usage_percent Current CPU usage percentage (0-100)
# TYPE genlayer_node_cpu_usage_percent gauge
genlayer_node_cpu_usage_percent{component="node"} 15.5
genlayer_node_cpu_usage_percent{component="genvm-llm"} 45.2
genlayer_node_cpu_usage_percent{component="genvm-web"} 12.8
genlayer_node_cpu_usage_percent{component="webdriver"} 5.3

# HELP genlayer_node_memory_rss_bytes Resident Set Size (RSS) memory usage in bytes
# TYPE genlayer_node_memory_rss_bytes gauge
genlayer_node_memory_rss_bytes{component="node"} 524288000
genlayer_node_memory_rss_bytes{component="genvm-llm"} 2147483648

# HELP genlayer_node_network_rx_bytes_total Total bytes received across all network interfaces
# TYPE genlayer_node_network_rx_bytes_total counter
genlayer_node_network_rx_bytes_total{component="node"} 1073741824
```

#### Available Metrics

The node collects metrics for three main components:
- **Node** - The main node process
- **GenVM** - The GenVM modules (LLM and Web)
- **WebDriver** - The WebDriver container

**Operator Condition Metrics**

- `genlayer_node_operator_condition_state{condition,state}`: one-hot gauge for the current catalog state. All state series are prewarmed at process start.
- `genlayer_node_operator_condition_occurrences_total{condition,reason}`: accepted failed observations by stable reason.
- `genlayer_node_operator_condition_transitions_total{condition,from,to}`: lifecycle transitions, including recovery.
- `genlayer_node_operator_condition_last_success_timestamp_seconds{condition}`: Unix timestamp of the last successful observation, or zero before one occurs.

`condition`, `state`, `reason`, `from`, and `to` come from the fixed startup catalog; upstream error text is never used as a label.

`occurrences_total` counts differently per condition kind, so the counter stays comparable: an *observed* condition (folded from duty traffic) counts every failed observation, while a *polled* condition counts only entry into a failing state. Counting every failed poll would scale the counter with the poll interval rather than the fault — a three-day ban at a ten-second poll would read as roughly 26,000 events.

**CPU Metrics**

**genlayer_node_cpu_usage_percent**

Current CPU usage percentage (0-100).

**Type:** Gauge

**Labels:**
- `component`: One of `node`, `genvm-llm`, `genvm-web`, `webdriver`

**Example:**
```
genlayer_node_cpu_usage_percent{component="node"} 15.5
genlayer_node_cpu_usage_percent{component="genvm-llm"} 45.2
genlayer_node_cpu_usage_percent{component="genvm-web"} 12.8
genlayer_node_cpu_usage_percent{component="webdriver"} 5.3
```

**Memory Metrics**

**genlayer_node_memory_rss_bytes**

Resident Set Size (RSS) memory usage in bytes.

**Type:** Gauge

**Labels:**
- `component`: One of `node`, `genvm-llm`, `genvm-web`, `webdriver`

**Example:**
```
genlayer_node_memory_rss_bytes{component="node"} 524288000
genlayer_node_memory_rss_bytes{component="genvm-llm"} 2147483648
```

**genlayer_node_memory_vms_bytes**

Virtual Memory Size (VMS) in bytes.

**Type:** Gauge

**Labels:**
- `component`: One of `node`, `genvm-llm`, `genvm-web`, `webdriver`

**Note:** Only available for node and GenVM components.

**genlayer_node_memory_usage_bytes**

Current memory usage in bytes (WebDriver only).

**Type:** Gauge

**Labels:**
- `component`: `webdriver`

**genlayer_node_memory_limit_bytes**

Memory limit in bytes (WebDriver only).

**Type:** Gauge

**Labels:**
- `component`: `webdriver`

**genlayer_node_memory_percent**

Memory usage percentage.

**Type:** Gauge

**Labels:**
- `component`: One of `genvm-llm`, `genvm-web`, `node`, `webdriver`
- `node`: Node address (hex format)

**genlayer_node_memory_peak_bytes**

Peak memory usage in bytes (GenVM components only).

**Type:** Gauge

**Labels:**
- `component`: One of `genvm-llm`, `genvm-web`
- `node`: Node address (hex format)

**Network Metrics**

**genlayer_node_network_rx_bytes_total**

Total bytes received across all network interfaces.

**Type:** Counter

**Labels:**
- `component`: One of `node`, `genvm-llm`, `genvm-web`, `webdriver`
- `node`: Node address (hex format)

**Example:**
```
genlayer_node_network_rx_bytes_total{component="node",node="0x84b6cbd007511352d3fea26834c5c39a440903c4"} 96879942
genlayer_node_network_rx_bytes_total{component="webdriver",node="0x84b6cbd007511352d3fea26834c5c39a440903c4"} 84
```

**genlayer_node_network_tx_bytes_total**

Total bytes transmitted across all network interfaces.

**Type:** Counter

**Labels:**
- `component`: One of `node`, `genvm-llm`, `genvm-web`, `webdriver`
- `node`: Node address (hex format)

**Disk Metrics**

**genlayer_node_disk_free_bytes**

Available free disk space in bytes.

**Type:** Gauge

**Labels:**
- `component`: `node`
- `directory`: One of `genlayer.db`, `keystore`, `logs`
- `node`: Node address (hex format)

**genlayer_node_disk_total_bytes**

Total disk space in bytes.

**Type:** Gauge

**Labels:**
- `component`: `node`
- `directory`: One of `genlayer.db`, `keystore`, `logs`
- `node`: Node address (hex format)

**genlayer_node_disk_usage_bytes**

Used disk space in bytes.

**Type:** Gauge

**Labels:**
- `component`: `node`
- `directory`: One of `genlayer.db`, `keystore`, `logs`
- `node`: Node address (hex format)

**genlayer_node_disk_usage_percent**

Used disk space as percentage.

**Type:** Gauge

**Labels:**
- `component`: `node`
- `directory`: One of `genlayer.db`, `keystore`, `logs`
- `node`: Node address (hex format)

**Node Status Metrics**

**genlayer_node_blocks_behind**

Number of blocks behind the latest block.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_latest_block**

Latest block number in the network.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_processing_block**

Currently processing block number.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_synced_block**

Last synced block number.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_synced**

Node synchronization status (1 = synced, 0 = not synced).

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_prime_blocked_insufficient_funds**

Whether a mandatory validator prime is currently blocked by an unfunded operator wallet (1 = blocked, 0 = not blocked). While blocked the validator cannot pay for consensus duty transactions and is flagged idle each round — fund the operator wallet to recover.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_uptime_seconds**

Node uptime in seconds.

**Type:** Gauge

**Labels:**
- `component`: `node`
- `node`: Node address (hex format)

**Transaction Metrics**

**genlayer_node_transactions_accepted_synced_total**

Total number of accepted and synced transactions.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_transactions_activated_total**

Total number of activated transactions.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_transactions_leader_proposed_total**

Total number of transactions proposed as leader.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_transactions_leader_revealed_total**

Total number of transactions revealed as leader.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_transactions_validator_commit_total**

Total number of validator commit votes.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**genlayer_node_transactions_validator_reveal_total**

Total number of validator reveal votes.

**Type:** Gauge

**Labels:**
- `node`: Node address (hex format)

**Gas Metrics (duty transactions)**

Emitted inline from the shared consensus gas estimator and transactor, so they cover every duty method (`finalizeTransaction`, `commitVote`, `revealVote`, `proposeReceiptWithFees`, `topUpAndSubmitAppeal`, `activateTransaction`, `validatorPrime`, `epochAdvance`, ...). All are true counters.

**genlayer_node_gas_estimation_total**

Outcome of every duty gas estimation.

**Type:** Counter

**Labels:**
- `method`: Contract method name
- `outcome`: One of `success` (real RPC estimate), `fallback_block_limit` (estimation failed; submitted at full block gas limit), `fallback_cache` / `fallback_cache_scaled` (estimation failed; reused gas from the shared calldata-length-keyed cache, possibly learned from a different method)

**genlayer_node_gas_estimation_capped_total**

Estimation succeeded but the safety-multiplied estimate (or per-call reserve) exceeded the block gas limit and was silently capped — the safety margin is gone while everything still works.

**Type:** Counter

**Labels:**
- `method`: Contract method name

**genlayer_node_gas_shortfall_total**

Confirmed on-chain out-of-gas: the transaction landed but ran out of gas mid-execution. Self-heals — the node retries with a larger gas limit — so a single occurrence is not actionable; a **sustained rate** signals systematic under-estimation (treat as a warning, not a page).

**Type:** Counter

**Labels:**
- `method`: Contract method name
- `reason`: One of `typed_shortfall` (`InsufficientGasForInternalMessageCreation` revert with exact gas requirement), `out_of_gas` (explicit `OutOfGas` revert), `heuristic` (undecoded revert that consumed gas above the exhaustion threshold)

**genlayer_node_gas_block_limit_exhausted_total**

Gas shortfall while the gas limit was already capped at the block limit: the work cannot fit in one block and retrying cannot help. Non-self-healing — **any occurrence is actionable** (page-worthy).

**Type:** Counter

**Labels:**
- `method`: Contract method name

**genlayer_node_gas_spent_wei_total**

Cumulative wei actually billed for gas, from each mined duty receipt (`gasUsed × effectiveGasPrice`). Reverted attempts are billed too and are accounted per attempt; submissions that never land cost nothing. The `reverted` slice is the exact cost of failed work, and `sum(genlayer_node_gas_spent_wei_total)` is the operator's total billed duty gas.

**Type:** Counter

**Labels:**
- `method`: Contract method name
- `outcome`: One of `success`, `reverted`
- `price_source`: `effective` (receipt's billed price) or `fee_cap` (submitted fee cap used as an upper bound when the RPC omits `effectiveGasPrice`)

**Consensus Submission Metrics**

**genlayer_node_consensus_submit_total**

Terminal outcome of every duty submission.

**Type:** Counter

**Labels:**
- `method`: Contract method name
- `outcome`: One of `success` (landed and executed), `reverted` (landed, contract rejected — gas still billed), `failed` (never landed: RPC/nonce/funds/authorization)

**genlayer_node_consensus_revert_total**

Decoded revert causes, one increment per observed revert (attempt-level).

**Type:** Counter

**Labels:**
- `method`: Contract method name
- `error`: Decoded custom error from the fee/gas allowlist (`InsufficientFeesForRound`, `InsufficientFees`, `FailedFeeTransfer`, `FeeSettlementNotCompleted`, `MaxPriceExceeded`, `ExecutionBudgetExceeded`, `MessageBudgetExceeded`, `BudgetTooLow`, `RollupBudgetBelowFloor`, `InsufficientBondCustody`, `InvalidAppealBond`, `AppealBondTooLow`, `InsufficientGasForInternalMessageCreation`, `OutOfGas`); anything else collapses to `other`

**Fee Flow Metrics (v0.6 ConsensusMainWithFees)**

Note: there are no dedicated appeal-bond metrics. No node runtime path submits appeals — appellants are external clients funding bonds from their own wallets. If a validator appeal duty ever ships, the method-labeled gas/submission metrics above cover it under `method="topUpAndSubmitAppeal"` with no code change.

**genlayer_node_fees_operator_insufficient_funds_total**

Duty submissions rejected because the operator account cannot cover value + gas. Complements the `genlayer_node_prime_blocked_insufficient_funds` gauge by leaving a trace of dips that recover between scrapes. A sustained increase means the operator wallet needs funding (page-worthy).

**Type:** Counter

**genlayer_node_fees_settlement_events_total**

Fee-settlement events decoded from this node's own mined finalize receipts. Finalize runs under a staggered lease, so each finalize is observed by exactly one node — aggregate fleet-wide with `sum by (network)`. Use the `severity` label to alert on failures without enumerating event names: `genlayer_node_fees_settlement_events_total{severity="alarm"}` covers all three failure modes and any future one.

**Type:** Counter

**Labels:**
- `event`: One of `UnifiedFeesDistributed` (fees distributed at settlement), `FeesRefunded` (surplus returned to the sender), `FeeSettlementFailed` (finalize succeeded but settlement failed — validators unpaid until `retryFeeSettlement`), `CommitFinalFeesFailed` (tx finalized without fee settlement), `ValueWithdrawalFailed` (a best-effort transfer — refund/appealer payout — failed; funds stay custodied pending recovery)
- `severity`: `alarm` for the three failure events (unpaid or stuck-funds modes needing operator action) or `info` for the normal `UnifiedFeesDistributed` / `FeesRefunded` happy path

**genlayer_node_fees_distributed_wei_total**

Total wei moved at fee settlement across **all** recipients — committee payouts **plus** sender/contributor refunds **plus** appealer rewards — summed from `UnifiedFeesDistributed` amounts in this node's finalize receipts. It **excludes** amounts retained in staking custody (`amountToStaking`). This is a network **"value settled"** liveness signal, **not** per-node earnings — for a validator's own income use `genlayer_node_fees_earned_wei_total`.

**Type:** Counter

**genlayer_node_fees_earned_wei_total**

This node's validator earnings credited at prime, read from the `ValidatorPrime` event when a prime is effective. This is the accurate **per-node** income (unlike `fees_distributed_wei_total`, which is whole-committee plus refunds). Earnings settle into stake per epoch, so this series lags settlement by up to one prime interval. Pair `type="fee_rewards"` with `genlayer_node_gas_spent_wei_total` for operator net P&L (`earned − gas_spent`).

**Type:** Counter

**Labels:**
- `type`: One of `fee_rewards` (fee-flow income), `validator_rewards`, `delegator_rewards` (staking-reward components)

**genlayer_node_fees_penalties_wei_total**

This node's fee penalties deducted at prime, read from the same `ValidatorPrime` event. Kept separate from `fees_earned_wei_total` so earnings stay gross income; subtract this for net.

**Type:** Counter

**genlayer_node_external_message_delivery_total**

External messages this node's own delivery calls (`flushExternalMessages`, `flushInternalMessages`, `finalizeTransaction`) resolved or paused, read from their mined receipts. A paused message was not called and not charged; the node retries it with a backoff (30 s doubling to 10 min) until the chain reports it issued. A `paused_*` series that keeps rising for one network means a message nobody can deliver.

**Type:** Counter

**Labels:**
- `outcome`: `delivered` (ran and succeeded), `call_failed` (ran and failed; final), `paused_gas` (no room to forward its gas envelope), `paused_value` (its value was not yet available), `cancelled_unfunded` (its open-bucket envelope was never funded; value refunded), `cancelled_ceiling` (its gas limit is above the current ceiling; value refunded)

**genlayer_node_external_message_reimbursed_wei_total**

Wei the chain reimbursed this node for running external messages: gas used times the transaction's locked gas price. Pair it with `gas_spent_wei_total` for the net cost of delivery.

**Type:** Counter

**Go Runtime Metrics**

The endpoint also exposes standard Go runtime metrics including:

- `go_gc_duration_seconds` - Garbage collection duration summary
- `go_goroutines` - Number of active goroutines
- `go_memstats_*` - Go memory statistics (heap, stack, GC)
- `go_threads` - Number of OS threads

**Process Metrics**

Standard process metrics are also available:

- `process_cpu_seconds_total` - Total CPU time
- `process_resident_memory_bytes` - Resident memory size
- `process_virtual_memory_bytes` - Virtual memory size
- `process_open_fds` - Number of open file descriptors
- `process_network_receive_bytes_total` - Network bytes received
- `process_network_transmit_bytes_total` - Network bytes transmitted

**Prometheus Handler Metrics**

Metrics about the metrics endpoint itself:

- `promhttp_metric_handler_requests_total` - Total scrape requests by status code
- `promhttp_metric_handler_requests_in_flight` - Current scrapes being served

#### Configuration

Metrics collection can be configured in the node configuration file:

```yaml
metrics:
  interval: "15s"        # Default interval for all collectors
  collectors:
    node:
      enabled: true      # Enable/disable node metrics
      interval: "30s"    # Optional: Override default interval
    genvm:
      enabled: true      # Enable/disable GenVM metrics
      # Uses default interval (15s)
    webdriver:
      enabled: true      # Enable/disable WebDriver metrics
      interval: "60s"    # Optional: Override default interval
```

**Default Values**
- `metrics.interval`: 15s (applies to all collectors by default)
- `enabled`: true (for each collector)
- `interval`: Uses metrics.interval if not specified per collector

**Configuration Priority**
1. Collector-specific `interval` (if specified)
2. Global `metrics.interval` (if specified)
3. System default (15s)

#### Troubleshooting

**No Metrics for GenVM**

GenVM processes are ephemeral and only run when executing smart contracts. If you don't see GenVM metrics, it's likely that no contracts are currently being executed.

**WebDriver Metrics Missing**

Ensure the WebDriver container is running:
```bash
docker ps | grep genlayer-node-webdriver
```

If not running, start it:
```bash
task docker:webdriver:docker-compose:up:detach
```

**Metrics Not Updating**

Check if metrics collection is enabled in your configuration and that the node has been restarted after configuration changes.

#### Performance Considerations

- Collection intervals can be increased to reduce overhead further
- Each collector runs independently and won't block others if one fails

### snapshot

Produces a consistent snapshot of the node's state at the current block. Two modes selected by the request body: stream the snapshot back as a gzipped tar response (operator path), or write it to a destination directory on the node's filesystem (zero-downtime upgrade-controller path).

**Method:** `POST /snapshot`

**Authentication:** required. Send the operator token in either header:

```
Authorization: Bearer <token>
X-Admin-Auth: <token>
```

The token is the value of `GENLAYERNODE_NODE_OPERATOR_TOKEN` on the node process. When that variable is unset, the endpoint returns `401` to every caller regardless of header value (default-deny). The other ops routes (`/metrics`, `/health`, `/balance`) are unaffected and remain public.

**Network exposure:** the ops server is intended for in-host or in-cluster access (Prometheus scraping `/metrics`, k8s probes hitting `/health`). The endpoint may not be reachable from outside the host depending on the deployment's firewall. The recommended operator workflow is therefore one of:

- SSH onto the node and call the endpoint via `127.0.0.1:9153`, or
- Open an SSH tunnel from the operator workstation to the node (`ssh -L 9153:127.0.0.1:9153 <node>`) and call `localhost:9153` through it.

**Parameters:**

- `scope` (string, optional): Which stores the archive carries — `all` (default), `genvm` or `ops`. An absent scope means `all`, so a caller written before the store split keeps its behaviour. An unrecognised value is rejected with `400`.

  | Scope | Contents | Portable between nodes |
  |---|---|---|
  | `all` | both stores, operation store first | **No** |
  | `genvm` | chain-derived history only | **Yes** |
  | `ops` | this node's consensus record only | **No** |

  Only `genvm` may be given to another operator. `all` and `ops` carry the vote salts this node generated and the nonce ownership of its operator key; restoring either onto a different node would make that node act on votes it never cast and nonce slots it never signed. The node refuses to start against an operation store stamped with another operator, but that guard runs at startup, not at copy time — treat a non-portable archive accordingly.

  Every archive carries a `snapshot.json` manifest recording the format version, the scopes inside, the rollup block the GenVM store was captured at, and — for `all` and `ops` — the operator address that produced it.

- `destDir` (string, optional): Absolute path on the node's filesystem, which must resolve to a location inside the configured snapshot directory (`node.admin.snapshot_dir`, default `<datadir>/snapshots`). When set, switches to write-to-path mode; when omitted or empty, the snapshot is streamed back as `application/gzip`. Paths outside the snapshot directory are rejected with `400`.

**Returns:**
- Stream mode (`destDir` omitted): gzipped tar archive in the response body, `Content-Type: application/gzip`.
- Write-to-path mode (`destDir` set): empty body, `204 No Content`.

**Example Request — Stream Mode (operator):**

On the node (after `ssh <node>`):

```bash
curl -X POST http://127.0.0.1:9153/snapshot \
  -H "X-Admin-Auth: $GENLAYERNODE_NODE_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}' \
  -o snapshot.tar.gz
```

Or via SSH tunnel from a workstation (forwards `9153` to the node's loopback):

```bash
ssh -L 9153:127.0.0.1:9153 -N <node> &
curl -X POST http://localhost:9153/snapshot \
  -H "X-Admin-Auth: $GENLAYERNODE_NODE_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}' \
  -o snapshot.tar.gz
```

The response body is a stream — `curl` can write it to a file (as above) or pipe it through any tool that reads stdin, without staging on local disk.

**Example Response — Stream Mode:**

```
HTTP/1.1 200 OK
Content-Type: application/gzip
Content-Disposition: attachment; filename="snapshot.tar.gz"

<gzipped tar archive of the snapshot's files>
```

**Example Request — Write-to-Path Mode (upgrade controller):**

```bash
curl -X POST http://127.0.0.1:9153/snapshot \
  -H "X-Admin-Auth: $GENLAYERNODE_NODE_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"destDir": "/var/lib/genlayer/data/node/snapshots/bradbury-100"}'

Take only the portable half, to hand to another operator:

```bash
curl -sS -X POST http://localhost:9153/snapshot \
  -H "Authorization: Bearer $OPERATOR_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"scope": "genvm"}' -o genvm-snapshot.tar.gz
```
```

**Example Response — Write-to-Path Mode:**

```
HTTP/1.1 204 No Content
```

**HTTP Status Codes:**

- `200 OK`: Stream mode succeeded; body carries the gzipped tar archive.
- `204 No Content`: Write-to-path mode succeeded; snapshot is on disk at `destDir`.
- `400 Bad Request`: `destDir` is not absolute, resolves outside the configured snapshot directory, or already exists.
- `401 Unauthorized`: Missing, invalid, or empty operator token (also returned when `GENLAYERNODE_NODE_OPERATOR_TOKEN` is unset on the node).
- `500 Internal Server Error`: Snapshot creation failed (e.g., disk full, DB checkpoint error).

**Notes:**

- A snapshot may be several GB. In stream mode the connection stays open for the duration of the snapshot — configure reverse proxies / load balancers with adequate read timeouts and disable response buffering.
- In stream mode, a dropped connection leaves no on-disk artifact on the node; retry the request from scratch.
- In write-to-path mode, `destDir` must be an absolute path that does not yet exist and that resolves (after `filepath.Clean`) to a location inside the configured snapshot directory; the node creates it. Paths that escape the directory via `..` or point elsewhere on the filesystem are rejected with `400`. This bounds where an operator-token holder can write a checkpoint — still treat the operator token as a credential that can write under the node user's filesystem permissions within that directory.
- **Snapshot directory** (`node.admin.snapshot_dir`): bounds write-to-path mode. A relative value resolves under `datadir` (so the default `snapshots` is `<datadir>/snapshots`); an absolute value is used as-is, letting a node target a dedicated disk. The node creates the directory with `0700` permissions on first use if it does not exist.
- **Docker deploys**: in write-to-path mode, `destDir` (and the snapshot directory) is resolved inside the node container's filesystem, not on the host. The default `<datadir>/snapshots` falls under whatever volume the deployment mounts for `datadir`, so it is host-reachable wherever that volume maps. To use a different location, set `snapshot_dir` to a path that is itself volume-mapped identically on both sides, or stick with stream mode. Stream mode is unaffected — the response body is sent over HTTP regardless of where the node runs.
- The snapshot includes the full chain database (SSTables, MANIFEST, WAL) at the moment of capture. Treat the artifact as sensitive — anyone holding it can reconstruct the node's state.
- Token comparison is constant-time and length-padded so neither the value nor the length of the secret can be inferred from response timing.
- Rotation: update `GENLAYERNODE_NODE_OPERATOR_TOKEN` and restart the node. Tokens are read once at startup and not hot-reloaded.

**Forward-compatibility note:** when the zero-downtime upgrade controller (Phase 3) lands, `/snapshot` will additionally accept a second role (`upgrade`) so the controller can drive the snapshot + stop + start sequence with its own credential. The operator-token path documented here remains the same — no migration is required for operator tooling.

**cURL Example with Bearer header:**

```bash
curl -X POST http://127.0.0.1:9153/snapshot \
  -H "Authorization: Bearer $GENLAYERNODE_NODE_OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}' \
  -o snapshot.tar.gz
```
