You've reached the end of the Anchoring section
Back to the Anchoring Overview, or review the exact commitment in Hashing & Merkle Trees.
The point of anchoring is that a third party can prove a data point existed and is unaltered without needing to trust CORE-M. This page shows the two verification APIs, the manual procedure a verifier follows, the self-contained portable proof bundle, and what BEEF is. It assumes you know the canonical hash and Merkle path from Hashing & Merkle Trees.
To verify a point you need only:
device_id, timestamp (Unix seconds), and the
telemetry payload.txid, and BEEF transaction, from CORE-M’s
verification API, the PostgreSQL proof store, or a portable bundle shared
out-of-band.No access to CORE-M’s running services is required for the verification itself.
CORE-M exposes two endpoints that automate the lookup. See API Overview for auth and base URLs.
Use this when you already hold the 32-byte data_hash.
GET /api/v1/verify/hash/<data_hash_hex>Looks up the proof store and returns the proof for that hash. If no proof exists,
the response is NOT_FOUND with "no blockchain proof found for this data hash — it may be pending or nonexistent". If the same hash was anchored in more than one
batch (re-anchoring), the most recent proof — the one with the highest
block_height — is returned.
Use this when you have the original fields and want CORE-M to compute the hash for
you. It recomputes the double-SHA-256 leaf hash defined in
Hashing & Merkle Trees — the same algorithm as the
telemetry service — looks up the result, and returns the same response shape as
verify-by-hash. tenant_id is required: it is part of the hash preimage, and
the API returns InvalidArgument (“tenant_id is required”) without it.
curl -X POST https://<host>/api/v1/verify/raw \ -H "Content-Type: application/json" \ -d '{ "tenant_id": "T1", "device_id": "D1", "timestamp": "2024-03-21T05:46:40Z", "numeric_values": {"temperature": 22.5, "humidity": 65}, "string_values": {} }'The verify endpoints (verify/hash, verify/raw, verify/batch) are public —
no Authorization token is required, so any third party can independently verify a
data point. Only ListProofs (GET /api/v1/verify/proofs) requires authentication,
because it is tenant-scoped.
If the supplied data differs even slightly from what was anchored, the recomputed
hash will not match any stored proof and the response is NOT_FOUND.
Both endpoints return the same proof:
{ "txid": "c3d4e5f6a1b2...", "block_height": 850000, "block_hash": "00000000000000000abc...", "confirmed_at": "2024-03-21T08:31:12Z", "batch_id": "8f14e45f-ceea-467a-9f3b-2c1d4e5f6a7b", "merkle_path": [ { "hash": "9a0c...", "is_right": true }, { "hash": "4f7b...", "is_right": true } ], "beef_hex": "0100beef..."}merkle_path is an array of {hash, direction} entries (the direction is
expressed as is_right), and beef_hex is the hex-encoded BEEF transaction —
explained below.
A verifier does not have to trust the API’s verified flag — they can redo the
proof end to end. Four steps:
Recompute the data hash. From the original tenant_id, device_id,
timestamp (Unix seconds), and payload, compute the double-SHA-256 leaf hash
SHA256(SHA256(preimage)) exactly as in
Hashing & Merkle Trees — including the version
byte, the tenant_id/device_id length prefixes, and tenant_id itself.
Confirm it equals the data_hash in the proof. This proves the data has not
been altered.
Walk the Merkle path. Starting from data_hash, fold in each sibling using
its direction until you reach the root (double-SHA-256 at every step):
current = data_hashfor step in merkle_path: if step.is_right: current = SHA256(SHA256(current || step.hash)) else: current = SHA256(SHA256(step.hash || current))# current is the reconstructed Merkle rootCheck the OP_RETURN in the BEEF transaction. Decode the BEEF blob, find the
OP_FALSE OP_RETURN output, and read its payload:
"CORE-M" || merkle_root(32) || batch_id(16) || timestamp(8) || count(4).
Confirm the embedded merkle_root equals the root you reconstructed in step 2.
This proves your point is committed by this on-chain transaction.
Confirm block inclusion. The proof records the block_height and
block_hash that ARC reported when it confirmed the transaction. CORE-M does
not independently verify block inclusion or longest-chain membership
offline — confirmation trusts ARC’s reported block. To check inclusion
yourself, cross-reference the txid against any blockchain node or block explorer at
the claimed block_height. The block’s position in the chain is your lower
bound on when the data existed.
Take the point from Hashing & Merkle Trees:
device_id="D1", timestamp=1711000000, payload
{"temperature":22.5,"humidity":65}.
{"humidity":65,"temperature":22.5}, then build
the preimage 0x01 · tenant_id (length-prefixed, T1) · device_id
(length-prefixed, D1) · timestamp 0x0000000065FBC9C0 · the JCS payload
bytes, and take SHA256(SHA256(...)). For this point the leaf hash is
9774d8af1f5e5a0457275ec82b482e0e1b46f6fc421720a279d0e661f99755d2. Confirm it
equals the data_hash returned by the API.merkle_path entries above — both is_right — folding
the hash to the right at each step to reach the root.OP_RETURN payload from the BEEF transaction, read
the 32-byte merkle_root at offset 6, and check it matches your reconstructed
root.850000, and (optionally) that a block explorer agrees the block at height
850000 has hash 00000000000000000abc... and contains the txid.If all four steps pass, the point provably existed at the time of that block and has not changed since.
For any final data point, the API can return a self-contained portable proof
bundle (request include_bundle=true on verify-by-hash). It carries everything
an external verifier needs to validate the commitment without calling CORE-M
again after download.
The bundle contains:
| Field | Purpose |
|---|---|
canonical_hash_inputs | The exact device_id, Unix-seconds timestamp, and JCS payload used to compute the hash. |
merkle_path | The sibling hashes with directions, leaf to root. |
merkle_root | The reconstructed/committed root. |
txid | The anchoring transaction ID. |
raw_tx or beef_hex | The transaction, as BEEF or raw, for offline checking. |
block_header_chain | Block headers establishing inclusion and chain context. |
confirmation_depth | How many confirmations deep the transaction is. |
anchor_mode | merkle, per_event, or chain. |
verification_steps | Human-readable steps mirroring the procedure above. |
schema_version | Bundle schema version. |
The bundle validates against the published JSON schema, so a verifier can machine-check its structure before trusting its contents.
BEEF — Background Evaluation Extended Format — is a compact binary format associated with SPV (Simplified Payment Verification) proofs. CORE-M stores a Core-M-internal variant of BRC-62 BEEF: a single transaction (the anchoring transaction) plus, when available, its on-chain Merkle path (a BUMP). It does not bundle ancestor (funding) transactions or block headers, and it is not intended to be exchanged with external BEEF consumers — it is an internal storage format for the proof record.
CORE-M builds this blob when the anchoring transaction is confirmed and stores the
hex with the proof record. A verifier decodes it to extract the OP_RETURN and
read the committed merkle_root (step 3). Because the blob carries no block
headers, block inclusion is not independently re-verified from it — see
step 4.
Confirmed proofs are persisted in PostgreSQL (anchor_proofs): data_hash,
batch_id, merkle_path (JSONB), txid, beef_hex, block_height,
block_hash, confirmed_at, tenant_id, and device_id.
You've reached the end of the Anchoring section
Back to the Anchoring Overview, or review the exact commitment in Hashing & Merkle Trees.