Skip to content

Detect sensor-record tampering with hashes

Build and verify a hash-chained ledger of fictional cold-chain readings, expose the exact failure caused by an altered record, and replay its Merkle root.

Cloud Clara: I want you to trace each digest, name the first broken rule, and keep record integrity separate from sensor truth., your practice guide

Cloud Clara: I want you to trace each digest, name the first broken rule, and keep record integrity separate from sensor truth.
Predict the reading, then compare it with the measurement.

Python 3 in your browser (JupyterLite)

Python · no install

Tier 2 · Web · paste-in setup · No account

Version tested: Python 3.12.7 / Pyodide 0.27.6 in JupyterLite 0.6.4; Chromium 148.0.7778.96; captureSource playwright:jupyterlite. Date: 2026-09-10.

Open the notebook in your browser and run each Python cell; no install or account is needed.

Three ways to run: use JupyterLite here with no install; run main.py locally from the downloadable lab folder; or open the same notebook in Google Colab.

Open in your browser (new tab)

Steps

Screens captured against JupyterLite Python 3.12.7 / Pyodide 0.27.6; Chromium 148.0.7778.96 on 2026-09-10; the tool may have moved on — the text steps are the contract.

  1. 1 Step 1

    Do
    In the JupyterLite notebook, run the Step 1 cell to canonicalise one sensor record before hashing it.
    You will see
    Input: execute notebook step 1 with the record’s unchanged fixture, seed, and parameters. Observed: event=cold-001 sensor=probe-A celsius=3.8 Observed: observed=2026-09-10T08:00:00Z Observed: canonical={"celsius":3.8,"event":"cold-001","observed":"2026-09-10T08:00:00Z","sensor":"probe-A"}
    Why it matters
    Expected: STEP 1 canonical-record event=cold-001 algorithm=SHA-256 from this deterministic notebook step. Interpretation: A verifier needs a stable byte representation; hashing an ambiguous serialization makes later comparison unreliable. Action: Run python3 main.py --step 1 to canonicalise one sensor record before hashing it. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 1, captured after running the real Python cell.
    Step 1 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  2. 2 Step 2

    Do
    In the JupyterLite notebook, run the Step 2 cell to append four fictional readings to a hash chain.
    You will see
    Input: execute notebook step 2 with the record’s unchanged fixture, seed, and parameters. Observed: 0 cold-001 3.8 000000000000 a6a7a066153c Observed: 1 cold-002 4.1 a6a7a066153c 76ffb253a7a4 Observed: 2 cold-003 5.4 76ffb253a7a4 18ded23cc0c7
    Why it matters
    Expected: STEP 2 build-chain blocks=4 head=e3750e60807d from this deterministic notebook step. Interpretation: Showing both hashes makes the chain relationship inspectable instead of presenting only a final head digest. Action: Run python3 main.py --step 2 to append four fictional readings to a hash chain. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 2, captured after running the real Python cell.
    Step 2 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  3. 3 Step 3

    Do
    In the JupyterLite notebook, run the Step 3 cell to verify the untouched chain.
    You will see
    Input: execute notebook step 3 with the record’s unchanged fixture, seed, and parameters. Observed: 0 MATCH MATCH PASS Observed: 1 MATCH MATCH PASS Observed: 2 MATCH MATCH PASS
    Why it matters
    Expected: STEP 3 verify-pristine valid=YES checked=4 from this deterministic notebook step. Interpretation: A known-good run proves the verifier checks both content and linkage before the deliberate failures are introduced. Action: Run python3 main.py --step 3 to verify the untouched chain. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 3, captured after running the real Python cell.
    Step 3 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  4. 4 Step 4

    Do
    In the JupyterLite notebook, run the Step 4 cell to alter cold-003 from 5.4 C to 2.4 C without changing the stored hash.
    You will see
    Input: execute notebook step 4 with the record’s unchanged fixture, seed, and parameters. Observed: Tamper test: edit one stored reading without changing its hash Observed: block=2 event=cold-003 celsius_before=5.4 celsius_after=2.4 Observed: stored_hash=18ded23cc0c7d26447786d18ec6d138ad4cb35c74ac4a36d965e08aaa2014308
    Why it matters
    Expected: STEP 4 tamper-detected valid=NO first-failure=2 rule=content-hash from this deterministic notebook step. Interpretation: The failed block and violated rule identify the first inconsistent record rather than merely reporting that the ledger is invalid. Action: Run python3 main.py --step 4 to alter cold-003 from 5.4 C to 2.4 C without changing the stored hash. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 4, captured after running the real Python cell.
    Step 4 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  5. 5 Step 5

    Do
    In the JupyterLite notebook, run the Step 5 cell to recompute only the altered block's hash and verify again.
    You will see
    Input: execute notebook step 5 with the record’s unchanged fixture, seed, and parameters. Observed: Partial rewrite test: recompute only the altered block Observed: block=2 old_hash=18ded23cc0c7d26447786d18ec6d138ad4cb35c74ac4a36d965e08aaa2014308 Observed: block=2 new_hash=279056de15b8627c5f297462ec171829b3eee1cc58a88c8651d38b841882803d
    Why it matters
    Expected: STEP 5 link-failure valid=NO first-failure=3 rule=previous-hash from this deterministic notebook step. Interpretation: This demonstrates why hiding a historical edit requires rewriting every later link, not just the changed block. Action: Run python3 main.py --step 5 to recompute only the altered block's hash and verify again. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 5, captured after running the real Python cell.
    Step 5 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  6. 6 Step 6

    Do
    In the JupyterLite notebook, run the Step 6 cell to hash the same four records into a two-level Merkle tree.
    You will see
    Input: execute notebook step 6 with the record’s unchanged fixture, seed, and parameters. Observed: leaf cold-001=daac3aeaca1f leaf cold-002=1b555916c554 Observed: leaf cold-003=5a627e81c3ca leaf cold-004=3416a15f72bc Observed: pair left=e0d1e50fb8c6 pair right=76a223e31c42
    Why it matters
    Expected: STEP 6 merkle-build leaves=4 root=4ba0f0fe7ade from this deterministic notebook step. Interpretation: The intermediate hashes show how one compact root commits to the whole off-ledger batch. Action: Run python3 main.py --step 6 to hash the same four records into a two-level Merkle tree. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 6, captured after running the real Python cell.
    Step 6 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)
  7. 7 Step 7

    Do
    In the JupyterLite notebook, run the Step 7 cell to replay the Merkle root after changing cold-003.
    You will see
    Input: execute notebook step 7 with the record’s unchanged fixture, seed, and parameters. Observed: Changed record: cold-003 celsius 5.4 -> 2.4 Observed: anchored_root=4ba0f0fe7ade7eff22bda031dab869e7507af04f21bd8419e7d39424f88d0fd1 Observed: replayed_root=bc146e157733532d9bf2d34ccc1644c586f78ff661700261fd23589a3de75735
    Why it matters
    Expected: STEP 7 merkle-check match=NO changed-event=cold-003 from this deterministic notebook step. Interpretation: A root mismatch detects changed evidence, while the scope line prevents treating hash consistency as proof that the physical probe was truthful. Action: Run python3 main.py --step 7 to replay the Merkle root after changing cold-003. Boundary: this local fixture is not a live device, network, or site measurement.
    JupyterLite notebook output for hash-chain-tamper-detection step 7, captured after running the real Python cell.
    Step 7 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab)

Chapter checks

These questions refer to the chapter’s examples. Use the return links to review their answers.

  1. A gateway handles 10,000 meters that report once per minute. The team wants each raw reading to become a ledger transaction. Which redesign keeps the proof useful without turning the ledger into telemetry storage?

    Return to the chapter’s knowledge check
  2. A multi-party IoT system wants every raw sensor reading written directly to a shared ledger. What is the strongest architecture response?

    Return to the chapter’s knowledge check

Caution

All data is fictional. This exercise demonstrates tamper evidence, not consensus, signatures, key custody, sensor calibration, physical truth, privacy controls, or durable storage; never infer that a matching hash proves the measurement was correct.

Return to Blockchain Limitations for IoT · Browse Labs