Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 140 additions & 3 deletions block-node/block-node/block-node-on-chain-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ Three operator-visible steps. All three transactions also support deferred execu

Submit a `RegisteredNodeCreateTransactionBody`, signed by the new `admin_key`. On success, the transaction receipt carries the assigned `registered_node_id` - **record this value safely**; it is your handle for every subsequent update or delete. If lost, recover it by listing all Block Node registrations on the network and filtering by your `admin_key`, endpoint host, or description:

**Before you begin, gather:**

- **Externally reachable endpoint** - the hostname or public IP your Block Node is reachable at from outside your network, such as the address your load balancer or ingress exposes. Do not register internal cluster addresses such as `ClusterIP` services or pod IPs.
- **Admin key pair** - a fresh ED25519 key pair used to authorize all future updates and deletions. The SDK examples in [Submit the transaction](#submit-the-transaction) show how to generate one; for production, use an HSM or KMS and store the private key immediately after generating it.
- **Funded payer account** - a Hiero account with enough HBAR to cover the transaction fee (approximately $0.09 at the pegged schedule rate; see [Fees and throttles](#fees-and-throttles)).

> **Mainnet only:** On Hedera mainnet, `RegisteredNodeCreate` is a privileged transaction. It can only be submitted by a payer account in the range `0.0.2`–`0.0.55`.

```bash
Expand Down Expand Up @@ -125,12 +131,104 @@ Splitting publish, subscribe, and status across separate endpoints lets each pat

#### Submit the transaction

Two paths:
Two paths to submit the create transaction:

- **`yahcli`** - the DevOps CLI bundled with consensus-node. The `registerednodes create / update / delete` subcommands wrap the three transactions directly. Source at [`hedera-node/yahcli/.../commands/registerednodes/`](https://github.com/hiero-ledger/hiero-consensus-node/tree/main/hedera-node/yahcli/src/main/java/com/hedera/services/yahcli/commands/registerednodes); usage in [`hedera-node/yahcli/README.md`](https://github.com/hiero-ledger/hiero-consensus-node/blob/main/hedera-node/yahcli/README.md).
- **Any official Hiero SDK** - all seven expose `RegisteredNodeCreateTransaction` (and Update / Delete equivalents): [Java](https://github.com/hiero-ledger/hiero-sdk-java), [JavaScript](https://github.com/hiero-ledger/hiero-sdk-js), [Go](https://github.com/hiero-ledger/hiero-sdk-go), [Rust](https://github.com/hiero-ledger/hiero-sdk-rust), [Swift](https://github.com/hiero-ledger/hiero-sdk-swift), [C++](https://github.com/hiero-ledger/hiero-sdk-cpp), [Python](https://github.com/hiero-ledger/hiero-sdk-python). All SDKs also expose `PrivateKey.generateED25519()` (or the language-equivalent) for `admin_key` generation; an external keygen tool is only needed if your operational policy requires one (HSM, KMS, etc.). The [Hiero SDKs index](https://docs.hiero.org/sdks) is the entry point for SDK-specific guides.
- **Any official Hiero SDK** - all seven expose `RegisteredNodeCreateTransaction` (and the Update / Delete equivalents): [Java](https://github.com/hiero-ledger/hiero-sdk-java), [JavaScript](https://github.com/hiero-ledger/hiero-sdk-js), [Go](https://github.com/hiero-ledger/hiero-sdk-go), [Rust](https://github.com/hiero-ledger/hiero-sdk-rust), [Swift](https://github.com/hiero-ledger/hiero-sdk-swift), [C++](https://github.com/hiero-ledger/hiero-sdk-cpp), [Python](https://github.com/hiero-ledger/hiero-sdk-python).

The Java and JavaScript examples below implement the endpoint set from the [worked example](#worked-example) above. Initialize your `client` with your operator account ID and key before running either example; see the [Hiero SDKs index](https://docs.hiero.org/sdks) for per-language setup guides.

**Java** (`com.hedera.hashgraph:sdk`):

```java
import com.hedera.hashgraph.sdk.*;
import java.util.List;

// Generate the admin key. Store the private key in secure storage before proceeding.
PrivateKey adminKey = PrivateKey.generateED25519();

BlockNodeServiceEndpoint publishEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40984)
.setRequiresTls(true)
.setEndpointApis(List.of(BlockNodeApi.PUBLISH));

BlockNodeServiceEndpoint subscribeEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40980)
.setRequiresTls(true)
.setEndpointApis(List.of(BlockNodeApi.SUBSCRIBE_STREAM));

BlockNodeServiceEndpoint statusEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40982)
.setRequiresTls(true)
.setEndpointApis(List.of(BlockNodeApi.STATUS));

TransactionReceipt receipt = new RegisteredNodeCreateTransaction()
.setAdminKey(adminKey)
.setDescription("acme-mainnet-1")
.addServiceEndpoint(publishEndpoint)
.addServiceEndpoint(subscribeEndpoint)
.addServiceEndpoint(statusEndpoint)
.freezeWith(client)
.sign(adminKey)
.execute(client)
.getReceipt(client);

// Record this value. It is required for every subsequent update or deletion.
long registeredNodeId = receipt.registeredNodeId;
System.out.println("registered_node_id: " + registeredNodeId);
```

The flow is the same regardless of path: build the transaction body using the fields from the [Worked example](#worked-example), sign with the `admin_key`, execute against the target network's gRPC endpoint, and read the `TransactionReceipt` to capture the assigned `registered_node_id`.
**JavaScript** (`@hiero-ledger/sdk`):

```javascript
import {
BlockNodeApi,
BlockNodeServiceEndpoint,
PrivateKey,
RegisteredNodeCreateTransaction,
} from "@hiero-ledger/sdk";

// Generate the admin key. Store the private key in secure storage before proceeding.
const adminKey = PrivateKey.generateED25519();

const publishEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40984)
.setRequiresTls(true)
.setEndpointApis([BlockNodeApi.Publish]);

const subscribeEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40980)
.setRequiresTls(true)
.setEndpointApis([BlockNodeApi.SubscribeStream]);

const statusEndpoint = new BlockNodeServiceEndpoint()
.setDomainName("bn.example.com")
.setPort(40982)
.setRequiresTls(true)
.setEndpointApis([BlockNodeApi.Status]);

const createTx = await new RegisteredNodeCreateTransaction()
.setAdminKey(adminKey.publicKey)
.setDescription("acme-mainnet-1")
.addServiceEndpoint(publishEndpoint)
.addServiceEndpoint(subscribeEndpoint)
.addServiceEndpoint(statusEndpoint)
.freezeWith(client)
.sign(adminKey);

const receipt = await (await createTx.execute(client)).getReceipt(client);

// Record this value. It is required for every subsequent update or deletion.
const registeredNodeId = receipt.registeredNodeId;
console.log("registered_node_id:", registeredNodeId.toString());
```

For other SDKs (Go, Rust, Swift, C++, Python), the pattern is the same: build the transaction, sign with the `admin_key`, execute against the target network, and read `registered_node_id` from the receipt. The full lifecycle example for each SDK is linked from the [Hiero SDKs index](https://docs.hiero.org/sdks).

#### Verify the registration

Expand Down Expand Up @@ -246,6 +344,45 @@ Three surfaces are exposed by the existing Hiero infrastructure once you are reg
- **Automatic Mirror Node pickup.** Mirror Nodes running with `hiero.mirror.importer.block.autoDiscoveryEnabled = true` pick up your registration from the registry without any per-Mirror-Node configuration change - see [hiero-mirror-node#13013](https://github.com/hiero-ledger/hiero-mirror-node/issues/13013) and the [Mirror Node Integration](./mirror-node-integration.md) guide for the consumer side.
- **Consensus node address book.** The existing `/api/v1/network/nodes` endpoint now includes an `associated_registered_nodes` field on each consensus-node entry, listing the registered nodes operated by the same entity.

## Troubleshooting

### Transaction returns `INVALID_ADMIN_KEY`

The transaction was not signed by the `admin_key` declared in the transaction body, or the key does not satisfy the `KeyList` / `ThresholdKey` threshold. Verify that:

1. The key passed to `.setAdminKey(...)` and the key used to `.sign(...)` are the same key pair.
2. For a multi-sig `admin_key`, enough members have signed to meet the threshold before you call `execute`.
3. You have not confused the operator key (the account paying the fee) with the `admin_key` (the key that controls the registration). Both sign the transaction, but they serve different roles.

### gRPC returns UNIMPLEMENTED (code 12)

The target network is running a Consensus Node version that pre-dates HIP-1137. The gRPC methods `AddressBookService/createRegisteredNode`, `updateRegisteredNode`, and `deleteRegisteredNode` are available from Consensus Node `v0.75` onward. Check the [Consensus Node release page](https://github.com/hiero-ledger/hiero-consensus-node/releases) and the [Availability across networks](#availability-across-networks) table above.

### Transaction is throttled (`BUSY` or `THROTTLED_AT_CONSENSUS`)

`RegisteredNodeCreate` shares a throttle bucket with `CryptoCreate` and `NodeCreate`, capped at approximately 2 ops/sec network-wide. If you are onboarding multiple Block Nodes, space create submissions by at least 1 second and implement exponential back-off on throttle rejections. `RegisteredNodeUpdate` and `RegisteredNodeDelete` are not separately rate-limited.

### `RegisteredNodeCreate` rejected on mainnet

On Hedera mainnet, `RegisteredNodeCreate` is a privileged transaction - only accounts in the range `0.0.2`–`0.0.55` may be the transaction payer. Confirm the account you set as the operator (the fee-payer) is in that range. On previewnet and testnet, any funded account may be the payer.

### Registration is not visible in the Mirror Node REST API

Mirror Nodes index the registry from block-stream data. Allow up to 30 seconds after a `SUCCESS` receipt before treating a missing entry as a failure. If the entry does not appear after a minute, confirm the Mirror Node version is `v0.156` or later (the release that adds `/api/v1/network/registered-nodes`).

### `RegisteredNodeDelete` returns `REGISTERED_NODE_STILL_ASSOCIATED`

If the `registered_node_id` you are deleting is still listed in a consensus node's `associated_registered_nodes`, the delete returns `REGISTERED_NODE_STILL_ASSOCIATED`. Remove the association first: submit `NodeUpdate` with the `registered_node_id` removed from the list (or with an empty list to clear all associations), wait for `SUCCESS`, then resubmit the delete.

### `registered_node_id` is unknown

If you lost the assigned ID, recover it by querying the Mirror Node for your endpoint hostname or admin key:

```bash
curl -s "https://{MIRROR_NODE_HOST}/api/v1/network/registered-nodes?type=BLOCK_NODE" \
| jq '.registered_nodes[] | select(.service_endpoints[]?.domain_name == "{YOUR_ENDPOINT_HOST}")'
```

## Backwards compatibility

The on-chain registry is entirely net-new functionality. Existing transactions, message types, and APIs are unaffected. A Block Node that does not register continues to function exactly as before - clients that already know its address will still connect. Only discoverability via the registry is gated on registration.
Expand Down
4 changes: 2 additions & 2 deletions block-node/block-node/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each plugin has its own properties, but this focuses on core options and core pl

| ENV Variable | Description | Default |
|:----------------------------------------|:-----------------------------------------------------------------------------------------------------------------------|:------------|
| SERVER_MAX_MESSAGE_SIZE_BYTES | Max message size (bytes) for HTTP/2. | 131,072,000 |
| SERVER_MAX_MESSAGE_SIZE_BYTES | Max message size (bytes) for HTTP/2. Also the cumulative byte ceiling for a single block on the publish stream. | 131,072,000 |
| SERVER_SOCKET_SEND_BUFFER_SIZE_BYTES | Send buffer size (bytes). | 131,072 |
| SERVER_SOCKET_RECEIVE_BUFFER_SIZE_BYTES | Receive buffer size (bytes). Override to 131072 for memory-constrained deployments (see `values-overrides/nano.yaml`). | 8,388,608 |
| SERVER_PORT | Default port for all services. Individual plugins may bind to a different port via their own config. | 40840 |
Expand Down Expand Up @@ -163,7 +163,7 @@ A more robust pattern for fully operator-managed plugins is to mount a pre-popul
| BACKFILL_END_BLOCK | Max block number, -1 means no limit. | -1 |
| BACKFILL_BLOCK_NODE_SOURCES_PATH | File path for BN sources (PBJ JSON `block-nodes.json`). | "" |
| BACKFILL_SCAN_INTERVAL | Scan interval for gap detection (ms). | 60000 |
| BACKFILL_MAX_RETRIES | Max retries to fetch a block. | 3 |
| BACKFILL_MAX_RETRIES | Max attempts to fetch a block, minimum 1. | 3 |
| BACKFILL_INITIAL_RETRY_DELAY | Initial retry delay (ms), grows linearly. | 5000 |
| BACKFILL_FETCH_BATCH_SIZE | Number of blocks per gRPC call. | 10 |
| BACKFILL_DELAY_BETWEEN_BATCHES | Delay (ms) between block batches. | 1000 |
Expand Down
1 change: 1 addition & 0 deletions block-node/block-node/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Observes inbound streams from publishers.
| Counter | `publisher_flow_control_individual_pauses` | Publisher handler pauses due to per-handler message budget exhaustion |
| Counter | `publisher_flow_control_aggregate_pauses` | Intervals where aggregate message budget was exceeded and all handler budgets were withheld |
| Counter | `publisher_flow_control_penalties_applied` | Penalty pauses applied to handlers that repeatedly exhaust their budget |
| Counter | `publisher_disconnected_oversize` | Publishers disconnected because a block exceeded the cumulative per-block byte ceiling |

---

Expand Down
6 changes: 4 additions & 2 deletions block-node/design/backfill-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ Detect missing gaps in the stored block sequence and autonomously fetch missing

<dt>Chunk</dt>
<dd>A contiguous range of blocks fetched in a single operation, bounded by the peer's available range,
the configured fetchBatchSize, and the gap end.</dd>
the configured fetchBatchSize, and the gap end. Block 0 is the exception: it is always fetched in a
chunk of its own, so the TSS verification data it bootstraps is persisted before any later block
that needs it is fetched.</dd>

<dt>Dual Schedulers</dt>
<dd>Two independent schedulers (Historical and Live-Tail) that process gaps concurrently,
Expand Down Expand Up @@ -253,7 +255,7 @@ Properties are set via the Block Node configuration system (prefix: `backfill.`)
| `endBlock` | long | -1 | Last block (-1 = unlimited) |
| `blockNodeSourcesPath` | String | "" | Path to peer nodes JSON file |
| `scanInterval` | int | 60000 | Gap detection interval in ms |
| `maxRetries` | int | 3 | Max retry attempts per fetch |
| `maxRetries` | int | 3 | Max attempts per fetch (min 1) |
| `initialRetryDelay` | int | 5000 | Initial retry delay in ms |
| `fetchBatchSize` | int | 10 | Blocks per gRPC request |
| `delayBetweenBatches` | int | 1000 | Delay between batches in ms |
Expand Down
46 changes: 40 additions & 6 deletions block-node/design/block-stream-forward-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,38 @@ stays as it is.
**None of this changes.** The point of the design is only what happens for item
types that do not exist yet.

### The Single Field Invariant

A `BlockItem` is a protobuf `oneof`, so a valid item carries **exactly one
field**. The wire format cannot enforce this: any combination of fields parses
successfully, with fields unknown to the compiled schema preserved as unknown
fields. The hashing step therefore checks the invariant itself, before any
category placement:

- An item with a known type and **no** unknown fields is processed normally.
- An item with **no type set and exactly one unknown field** is a future item,
and the numbering rule below applies to it.
- An item with a known type **and** one or more unknown fields is a valid
encoding but not a processable stream, and the block is refused as an
unsupported stream format.
- An item with no type set and **more than one** unknown field is likewise
refused as an unsupported stream format.
- An item with **no field at all** carries nothing valid to process and is
refused as an unknown error.

None of these refusals are parse failures: the bytes are well formed protobuf
and parsing succeeds. They are structural violations detected after parsing.

### The Numbering Rule for Future Item Types

To maximize forward compatibility, and to minimize the need to coordinate
deployments of different systems creating and processing block streams in the
future, the block stream format requires the following rule for field numbering.

An unknown field numbered **below 20** is a first release field, reserved for
item types that require specific handling. A version that does not know such a
field cannot process it and refuses the block as an unsupported item type.

Fields numbered **20 and above** MUST be numbered so that:

```text
Expand Down Expand Up @@ -391,15 +418,15 @@ existing verification configuration is unchanged.

## Metrics

No new metrics are required, but a few would make forward compatibility events
visible to operators and give early warning of a needed upgrade:
Three counters make forward compatibility events visible to operators and give
early warning of a needed upgrade:

- a count of future item types placed into a subtree by the rule, including the
extension subtrees,
- a count of future items safely ignored as `not hashed`,
- a count of blocks refused because an item fell into a reserved category
(16 to 18), or needed specific handling (1 or 2) that this version does not
know.
- a count of future items refused because they fell into a reserved category
(16 to 18), needed specific handling (1 or 2) that this version does not
know, or carried an unknown first release field (below 20).

## Exceptions

Expand All @@ -413,7 +440,14 @@ failure** for the block instead.
specific handling" category (1 or 2) whose processing this version does not
know, causes the block to be refused, with a clear failure showing that an
upgrade is required. It is never silently dropped or placed elsewhere.
- A malformed or unexpected item that does not fit the rule at all is treated as a
- A future item carrying an unknown first release field (below 20) is refused
the same way, as an unsupported item type.
- An item that violates the single field invariant (a known type together with
unknown fields, or several unknown fields at once) parses successfully but is
refused as an **unsupported stream format**; an item with no field at all is
refused as an **unknown error**. These are structural violations, not parse
failures.
- A malformed item whose bytes cannot be decoded at all is treated as a
**parse failure**, as it is today.

## Acceptance Tests
Expand Down
Loading
Loading