Light client
Check whether a block touches your addresses or assets without downloading it, and without trusting the node that serves the data. Background: Light clients & filters.
What the example does
- Fetches filter headers (
getFilterHeaders,GET /v1/filters/headers) and each block's filter (getBlockFilter,GET /v1/blocks/{id}/filter): the block hash, height and parent hash, the filter, its filter header, the coinbase and the coinbase's merkle branch. - Verifies the filter against the filter-header chain, and the coinbase carrying the
"ARKF"commitment against the block's merkle root, so the filter is covered by proof of work. The TypeScript SDK bundles both checks asverifyBlockFilterProof; the building blocks areverifyFilterandverifyMerkleBranch. - Tests the watched address's lock against the filter locally with
matchFilter; the node never learns what is watched. Only matching blocks need downloading. - Verifies the latest signed checkpoint against the network's signer group with
verifyCheckpoint, so the client knows which blocks are final.
/**
* Light client: verify a block filter against its header and coinbase commitment, test it
* for our address, and verify the latest signed checkpoint.
*
* ARREKTA_URL=http://127.0.0.1:28332 npx tsx examples/light-client.ts
*/
import {
createClient,
decodeAddress,
fromHex,
getBlock,
getBlockFilter,
getChain,
getCheckpoints,
getFilterHeaders,
keychainFromSeed,
matchFilter,
networkSignerSet,
verifyBlockFilterProof,
verifyCheckpoint,
} from 'arrekta-sdk';
const URL = process.env.ARREKTA_URL ?? 'http://127.0.0.1:28332';
const SEED = fromHex(process.env.ARREKTA_SEED ?? 'a1'.repeat(32));
// 1. Connect and pick the tip.
const client = createClient({ url: URL });
const chain = await getChain(client);
const wallet = keychainFromSeed(SEED, { network: chain.network });
const height = chain.height;
// 2. Filter headers: the tip's header must chain from its parent's.
const { headers } = await getFilterHeaders(client, { start: Math.max(height - 1, 0), count: 2 });
const prevHeader = height > 0 ? headers[0]!.filter_header! : '00'.repeat(32);
// 3. Fetch the tip's filter and verify it (coinbase commitment, merkle branch, header).
const f = await getBlockFilter(client, { id: height });
const header = await getBlock(client, { id: height, txs: 'none' });
// the proof's merkle root must be the PoW-checked header's
const ok = header.merkle_root === f.merkle_root && verifyBlockFilterProof(f, prevHeader).ok;
console.log(`block ${height}: filter verified = ${ok}`);
// 4. Test the filter locally for our address (the node never learns what we watch).
const mine = matchFilter(f.prev_hash, f.filter, [decodeAddress(wallet.address(0), chain.network)]);
console.log(`block ${height} may pay ${wallet.address(0)}: ${mine}`);
// 5. Verify the latest signed checkpoint against the network's signer group.
const { latest } = await getCheckpoints(client);
const signers = networkSignerSet(chain.network);
if (latest && signers) {
console.log(`checkpoint ${latest.height}: final = ${verifyCheckpoint(latest.hex, signers, chain.network).final}`);
} else {
console.log('no checkpoint yet');
}"""Light client: verify a block filter against its header and coinbase commitment, test it
for our address, and verify the latest signed checkpoint.
ARREKTA_URL=http://127.0.0.1:28332 python examples/light_client.py
"""
import os
from arrekta_sdk import (
Client,
keychain_from_seed,
match_filter,
network_signer_set,
verify_block_filter,
verify_checkpoint,
)
URL = os.environ.get("ARREKTA_URL", "http://127.0.0.1:28332")
SEED = bytes.fromhex(os.environ.get("ARREKTA_SEED", "a1" * 32))
def main() -> None:
# 1. Connect and pick the tip.
client = Client(URL)
chain = client.get_chain()
wallet = keychain_from_seed(SEED, network=chain["network"])
height = chain["height"]
# 2. Filter headers: the tip's header must chain from its parent's.
headers = client.get_filter_headers(max(height - 1, 0), 2)["headers"]
prev_header = headers[0]["filter_header"] if height > 0 else "00" * 32
# 3. Fetch the tip's filter and verify it (coinbase commitment, merkle branch, header).
f = client.get_block_filter(height)
header = client.get_block(height, "none")
ok = verify_block_filter(f, header=header, prev_filter_header=prev_header)
print(f"block {height}: filter verified = {ok}")
# 4. Test the filter locally for our address (the node never learns what we watch).
mine = match_filter(f["prev_hash"], f["filter"], [wallet.address(0)])
print(f"block {height} may pay {wallet.address(0)}: {mine}")
# 5. Verify the latest signed checkpoint against the network's signer group.
latest = client.get_checkpoints()["latest"]
signers = network_signer_set(chain["network"])
if latest and signers:
print(f"checkpoint {latest['height']}: final = {verify_checkpoint(latest['hex'], signers, chain['network'])}")
else:
print("no checkpoint yet")
if __name__ == "__main__":
main()Source: sdk/typescript/examples/light-client.ts (updated 2026-11-02) · sdk/python/examples/light_client.py (updated 2026-11-02). Runnable against a regtest node; CI runs every example.
For many blocks, fetch filter headers in batches with getFilterHeaders({ start, count }) and
compare them across several nodes before downloading filters.