Skip to content

Querying Chain and Finalization Height⚓︎

The /chain/info GET endpoint returns the current chain height and the latest finalization height. Comparing both values shows how far behind the finalized state is from the chain tip, which is useful for applications that need to confirm transactions are irreversible.

This tutorial shows how to poll chain state in a loop and display how long ago each height last changed.

Prerequisites⚓︎

This tutorial uses the Symbol REST API without requiring an SDK. You only need a way to make HTTP requests.

Full Code⚓︎

import json
import os
import time
import urllib.request

NODE_URL = os.getenv(
    'NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')

prev_height = None
prev_finalized_height = None
height_changed_at = None
finalized_changed_at = None

try:
    while True:
        with urllib.request.urlopen(f'{NODE_URL}/chain/info') as response:
            chain_info = json.loads(response.read().decode())

        height = int(chain_info['height'])
        finalized = chain_info['latestFinalizedBlock']
        finalized_height = int(finalized['height'])

        now = time.time()

        if prev_height is not None and height != prev_height:
            height_changed_at = now
        if (
            prev_finalized_height is not None
            and finalized_height != prev_finalized_height
        ):
            finalized_changed_at = now

        if height_changed_at is not None:
            h_ago = f"{int(now - height_changed_at)}s ago"
        else:
            h_ago = "-"
        if finalized_changed_at is not None:
            f_ago = f"{int(now - finalized_changed_at)}s ago"
        else:
            f_ago = "-"

        print(
            f"Height: {height:>10,}"
            f"  (changed {h_ago})"
            f"  |  Finalized: {finalized_height:>10,}"
            f"  (changed {f_ago})"
        )

        prev_height = height
        prev_finalized_height = finalized_height
        time.sleep(1)

except Exception as error:
    print(error)

Download source

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log(`Using node ${NODE_URL}`);

let prevHeight = null;
let prevFinalizedHeight = null;
let heightChangedAt = null;
let finalizedChangedAt = null;

try {
    while (true) {
        const response = await fetch(`${NODE_URL}/chain/info`);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const chainInfo = await response.json();

        const height = parseInt(chainInfo.height, 10);
        const finalized = chainInfo.latestFinalizedBlock;
        const finalizedHeight = parseInt(finalized.height, 10);

        const now = Date.now();

        if (prevHeight !== null && height !== prevHeight) {
            heightChangedAt = now;
        }
        if (prevFinalizedHeight !== null
            && finalizedHeight !== prevFinalizedHeight) {
            finalizedChangedAt = now;
        }

        const hAgo = heightChangedAt !== null
            ? `${Math.floor((now - heightChangedAt) / 1000)}s ago`
            : '-';
        const fAgo = finalizedChangedAt !== null
            ? `${Math.floor((now - finalizedChangedAt) / 1000)}s ago`
            : '-';

        const h = height.toLocaleString().padStart(10);
        const fh = finalizedHeight.toLocaleString().padStart(10);
        console.log(
            `Height: ${h}  (changed ${hAgo})`
            + `  |  Finalized: ${fh}`
            + `  (changed ${fAgo})`
        );

        prevHeight = height;
        prevFinalizedHeight = finalizedHeight;
        await new Promise((resolve) => setTimeout(resolve, 1000));
    }
} catch (error) {
    throw error;
}

Download source

The snippet uses the NODE_URL environment variable to set the Symbol API node. If no value is provided, a default one is used.

The program runs in an infinite loop, printing a status line every second. A keyboard interrupt (Ctrl+C) stops the loop.

Code Explanation⚓︎

Fetching Chain Information⚓︎

        with urllib.request.urlopen(f'{NODE_URL}/chain/info') as response:
            chain_info = json.loads(response.read().decode())

        height = int(chain_info['height'])
        finalized = chain_info['latestFinalizedBlock']
        finalized_height = int(finalized['height'])
        const response = await fetch(`${NODE_URL}/chain/info`);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const chainInfo = await response.json();

        const height = parseInt(chainInfo.height, 10);
        const finalized = chainInfo.latestFinalizedBlock;
        const finalizedHeight = parseInt(finalized.height, 10);

On each iteration, the code sends a GET request to the /chain/info GET endpoint. The response contains:

  • height: The current chain height (the latest block known to the node).
  • latestFinalizedBlock: An object with details about the most recently finalized block, including:
    • height: The finalized block height.
    • finalizationEpoch: The finalization epoch number.
    • finalizationPoint: The finalization point within the epoch.
    • hash: The hash of the finalized block.

The chain height increases each time a new block is produced (approximately every 30 seconds).

The finalized height lags behind the chain tip because a block is typically finalized 10 to 20 minutes after it is produced. When a finalization round completes, the finalized height jumps forward by many blocks at once rather than advancing one block at a time. See the Consensus textbook section for details on how voting nodes drive this process.

Tracking Height Changes⚓︎

        if prev_height is not None and height != prev_height:
            height_changed_at = now
        if (
            prev_finalized_height is not None
            and finalized_height != prev_finalized_height
        ):
            finalized_changed_at = now

        if height_changed_at is not None:
            h_ago = f"{int(now - height_changed_at)}s ago"
        else:
            h_ago = "-"
        if finalized_changed_at is not None:
            f_ago = f"{int(now - finalized_changed_at)}s ago"
        else:
            f_ago = "-"
        if (prevHeight !== null && height !== prevHeight) {
            heightChangedAt = now;
        }
        if (prevFinalizedHeight !== null
            && finalizedHeight !== prevFinalizedHeight) {
            finalizedChangedAt = now;
        }

        const hAgo = heightChangedAt !== null
            ? `${Math.floor((now - heightChangedAt) / 1000)}s ago`
            : '-';
        const fAgo = finalizedChangedAt !== null
            ? `${Math.floor((now - finalizedChangedAt) / 1000)}s ago`
            : '-';

To show how long ago each height last changed, the code stores the previous values and their timestamps. When a height differs from the previous value, the timestamp is updated to the current time.

Until a change is observed, the timestamp remains unset and the output displays - instead of a number. Once a change occurs, the counter starts from 0s ago and increments each second until the next change.

Polling Loop⚓︎

        print(
            f"Height: {height:>10,}"
            f"  (changed {h_ago})"
            f"  |  Finalized: {finalized_height:>10,}"
            f"  (changed {f_ago})"
        )

        prev_height = height
        prev_finalized_height = finalized_height
        time.sleep(1)
        const h = height.toLocaleString().padStart(10);
        const fh = finalizedHeight.toLocaleString().padStart(10);
        console.log(
            `Height: ${h}  (changed ${hAgo})`
            + `  |  Finalized: ${fh}`
            + `  (changed ${fAgo})`
        );

        prevHeight = height;
        prevFinalizedHeight = finalizedHeight;
        await new Promise((resolve) => setTimeout(resolve, 1000));

Each iteration prints a single status line showing:

  • The current chain height and how many seconds have elapsed since it last changed.
  • The finalized height and how many seconds have elapsed since it last changed.

The loop then sleeps for one second between iterations.

Output⚓︎

The following output shows a typical run monitoring the chain and finalization heights:

Using node https://reference.symboltest.net:3001
Height:  3,159,411  (changed -)  |  Finalized:  3,159,388  (changed -)
Height:  3,159,411  (changed -)  |  Finalized:  3,159,388  (changed -)
Height:  3,159,411  (changed -)  |  Finalized:  3,159,388  (changed -)
Height:  3,159,412  (changed 0s ago)  |  Finalized:  3,159,388  (changed -)
Height:  3,159,412  (changed 1s ago)  |  Finalized:  3,159,388  (changed -)
Height:  3,159,412  (changed 2s ago)  |  Finalized:  3,159,411  (changed 0s ago)
Height:  3,159,412  (changed 3s ago)  |  Finalized:  3,159,411  (changed 1s ago)

The output shows:

  1. The chain height advances 1 block, from 3,159,411 to 3,159,412. At that point, the change counter starts from 0s.
  2. Later, the finalized height catches up and advances 23 blocks, from 3,159,388 to 3,159,411. Its change counter starts from 0s too.
  3. Both heights initially show - because no change has been observed yet.

The gap between the chain height and the finalized height is normal. A transaction included in a block at the chain tip is confirmed but not yet irreversible. Once the finalized height reaches or exceeds that block, the transaction is guaranteed to remain in the chain.

Conclusion⚓︎

This tutorial showed how to:

Step Related documentation
Fetch chain information /chain/info GET