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.
Predict the reading, then compare it with the measurement.
Python 3 in your browser (JupyterLite)
Python · no installOpen 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.
Steps
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.

Step 1 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

Step 2 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

Step 3 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

Step 4 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

Step 5 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

Step 6 · Python 3 in your browser (JupyterLite); numbered callout added to a real capture. Enlarge screenshot (new tab) 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.

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.
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 checkA 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