Skip to content

feat: add getBlock method - #20

Open
Danil42Russia wants to merge 2 commits into
toncenter:masterfrom
Danil42Russia:danil42russia/getBlock_method
Open

feat: add getBlock method#20
Danil42Russia wants to merge 2 commits into
toncenter:masterfrom
Danil42Russia:danil42russia/getBlock_method

Conversation

@Danil42Russia

@Danil42Russia Danil42Russia commented Aug 7, 2026

Copy link
Copy Markdown

First and foremost, this PR depends on a new request in tonlib, and there is a PR for this: ton-blockchain/ton#2525

Conceptually, this is a continuation of the download method from https://explorer.toncoin.org

To test this, I wrote a Python script that compares the response with explorer.toncoin.org byte by byte

Python script
#!/usr/bin/env python3

import base64
import binascii
import json
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen


TIMEOUT_SECONDS = 30


def normalize_hex_hash(value, name):
    try:
        raw = bytes.fromhex(value)
    except ValueError:
        raise ValueError("{} must be a hexadecimal string".format(name))
    if len(raw) != 32:
        raise ValueError(
            "{} must contain exactly 32 bytes (64 hex characters)".format(name)
        )
    return value.upper()


def to_base64(hex_value):
    return base64.b64encode(bytes.fromhex(hex_value)).decode("ascii")


def shard_to_hex(shard):
    if shard < -(1 << 63) or shard >= (1 << 63):
        raise ValueError("shard must fit into a signed 64-bit integer")
    return "{:016X}".format(shard & ((1 << 64) - 1))


def add_query(url, parameters):
    separator = "&" if "?" in url else "?"
    return url + separator + urlencode(parameters)


def get_nested(data, path):
    value = data
    for key in path.split("."):
        if not isinstance(value, dict) or key not in value:
            return False, None
        value = value[key]
    return True, value


def main(api_url, explorer_url, workchain, shard, seqno, root_hash, file_hash):
    try:
        root_hash = normalize_hex_hash(root_hash, "root_hash")
        file_hash = normalize_hex_hash(file_hash, "file_hash")
        explorer_shard = shard_to_hex(shard)
    except ValueError as error:
        print("FAIL: {}".format(error), file=sys.stderr)
        return 1

    request_url = add_query(
        api_url,
        {
            "workchain": workchain,
            "shard": shard,
            "seqno": seqno,
            "root_hash": root_hash,
            "file_hash": file_hash,
        },
    )
    request = Request(request_url, headers={"Accept": "application/json"})

    expected_fields = {
        "ok": True,
        "result.@type": "blocks.blockData",
        "result.id.@type": "ton.blockIdExt",
        "result.id.workchain": workchain,
        "result.id.shard": str(shard),
        "result.id.seqno": seqno,
        "result.id.root_hash": to_base64(root_hash),
        "result.id.file_hash": to_base64(file_hash),
    }

    try:
        with urlopen(request, timeout=TIMEOUT_SECONDS) as response:
            body = response.read()
    except HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        print("FAIL: HTTP {}: {}".format(error.code, body), file=sys.stderr)
        return 1
    except URLError as error:
        print("FAIL: request failed: {}".format(error.reason), file=sys.stderr)
        return 1

    try:
        data = json.loads(body.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError) as error:
        print("FAIL: response is not valid UTF-8 JSON: {}".format(error), file=sys.stderr)
        return 1

    failures = []
    for path, expected in expected_fields.items():
        found, actual = get_nested(data, path)
        if not found:
            failures.append("{} is missing".format(path))
        elif actual != expected or type(actual) is not type(expected):
            failures.append(
                "{}: expected {!r}, got {!r}".format(path, expected, actual)
            )

    if failures:
        print("FAIL: getBlock response does not match:", file=sys.stderr)
        for failure in failures:
            print("  - {}".format(failure), file=sys.stderr)
        return 1

    for path, expected in expected_fields.items():
        print("OK: {} matches ({})".format(path, expected))

    found, encoded_data = get_nested(data, "result.data")
    if not found or not isinstance(encoded_data, str):
        print("FAIL: result.data is missing or is not a string", file=sys.stderr)
        return 1

    try:
        local_boc = base64.b64decode(encoded_data, validate=True)
    except (ValueError, binascii.Error) as error:
        print("FAIL: result.data is not valid Base64: {}".format(error), file=sys.stderr)
        return 1

    explorer_request_url = add_query(
        explorer_url,
        {
            "workchain": workchain,
            "shard": explorer_shard,
            "seqno": seqno,
            "roothash": root_hash,
            "filehash": file_hash,
        },
    )
    explorer_request = Request(
        explorer_request_url,
        headers={
            "Accept": "application/octet-stream",
            "User-Agent": "curl/8.7.1",
        },
    )

    try:
        with urlopen(explorer_request, timeout=TIMEOUT_SECONDS) as response:
            explorer_boc = response.read()
    except HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")[:1000]
        print(
            "FAIL: Explorer HTTP {}: {}".format(error.code, body),
            file=sys.stderr,
        )
        return 1
    except URLError as error:
        print(
            "FAIL: Explorer request failed: {}".format(error.reason),
            file=sys.stderr,
        )
        return 1

    if local_boc != explorer_boc:
        common_size = min(len(local_boc), len(explorer_boc))
        first_difference = next(
            (
                index
                for index in range(common_size)
                if local_boc[index] != explorer_boc[index]
            ),
            common_size,
        )
        print("FAIL: result.data differs from Explorer BOC", file=sys.stderr)
        print("  local size:    {} bytes".format(len(local_boc)), file=sys.stderr)
        print(
            "  Explorer size: {} bytes".format(len(explorer_boc)),
            file=sys.stderr,
        )
        print(
            "  first difference: byte {}".format(first_difference),
            file=sys.stderr,
        )
        return 1

    print("OK: result.data equals Explorer BOC ({} bytes)".format(len(local_boc)))
    print("OK: all checks passed")
    return 0


if __name__ == "__main__":
    sys.exit(
        main(
            api_url="http://127.0.0.1:8081/api/v2/getBlock",
            explorer_url="https://explorer.toncoin.org/download",
            workchain=-1,
            shard=-9223372036854775808,
            seqno=84439382,
            root_hash="ECEBCB21117D4EBD5876249B0DA0EE65D1D0D09F23ABE7CDF294D9DF70F5F7AE",
            file_hash="5E16EE71818ACA16AAE2819463A10C5A176445A18409081E553FA2066D45955F",
        )
    )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant