I Built A Tee Attested Log Proof Pipeline For Blockchain Node Metadata
Written by
Cipher Stone
I ran into a weird but real problem while tinkering with blockchain infrastructure: node operators (including me) often have “clean” logs—until you try to prove cryptographically that a given node really produced certain metadata at a specific time and configuration.
The usual options are either:
- trust the operator, or
- trust the blockchain’s own consensus history (which doesn’t always cover off-chain details like “this log was generated by that exact code/config on this host,” especially when you care about digital provenance beyond cryptocurrency).
So I built a tiny pipeline that uses a Trusted Execution Environment (TEE)—a hardware-protected enclave—to attest that log data was produced inside a measured environment. Then I turned that attestation plus the log content into a compact hash chain that can be anchored on-chain as digital provenance.
This post is about the very specific core idea that made it click for me: attesting node metadata without shipping raw secrets, while producing an on-chain verifiable proof.
The niche problem: “Prove the node created this metadata” without trusting it
In my case, I wanted to publish a provenance trail for “node metadata events” such as:
- software version
- chain ID
- boot time
- peer count
- config fingerprints
- a batch of structured “event logs”
But I didn’t want to:
- rely on the operator’s claim,
- leak sensitive config,
- or upload huge logs to the chain.
What I needed instead:
- A way to prove a host ran code in a protected environment (TEE attestation).
- A deterministic, verifiable digest of the log content.
- A way to batch events and anchor them efficiently on-chain.
What I implemented (high level)
I implemented a pipeline with three artifacts:
1) log.jsonl (event logs)
Each event is a JSON object on its own line.
2) A TEE attestation (simulated here)
Real TEEs provide vendor-specific attestation (e.g., Intel SGX DCAP, AMD SEV-SNP, ARM TrustZone attestation). Those are not easily runnable in a generic blog environment, so I mocked the attestation in code—while preserving the cryptographic shape:
- a measurement value (what code/config ran)
- a signature over that measurement and a nonce
3) A provenance “bundle hash” anchored on-chain
I compute:
log_root: a Merkle root over event hashesbundle_hash:SHA256(attestation_signature || measurement || nonce || log_root)
Then I store bundle_hash in an on-chain contract method record(bytes32 bundleHash, bytes32 logRoot).
Step 1: Build deterministic event hashing (Merkle root)
To avoid uploading whole logs, I used a Merkle tree. A Merkle tree is a structure that lets you prove “this event is included” using only a small set of hashes.
Here’s the code I used to:
- hash each event line
- compute the Merkle root
import hashlib import json from dataclasses import dataclass from typing import List def sha256(data: bytes) -> bytes: return hashlib.sha256(data).digest() def canonical_json(obj) -> bytes: # Deterministic JSON encoding so the same event always hashes the same way. return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") def hash_event_line(line: str) -> bytes: event = json.loads(line) return sha256(canonical_json(event)) def merkle_root(leaves: List[bytes]) -> bytes: if not leaves: return sha256(b"empty") level = leaves[:] while len(level) > 1: next_level = [] # If odd number of nodes, duplicate the last (common Merkle convention). for i in range(0, len(level), 2): left = level[i] right = level[i + 1] if i + 1 < len(level) else level[i] next_level.append(sha256(left + right)) level = next_level return level[0] if __name__ == "__main__": sample_lines = [ json.dumps({"ts": 1710000000, "event": "BOOT", "chain_id": "testnet-1"}, sort_keys=True), json.dumps({"ts": 1710000010, "event": "PEERS", "count": 12}, sort_keys=True), json.dumps({"ts": 1710000033, "event": "CONFIG", "fingerprint": "cfg_9a12"}, sort_keys=True), ] leaves = [hash_event_line(l) for l in sample_lines] root = merkle_root(leaves) print("log_root(hex) =", root.hex())
Why this matters
- Deterministic canonical JSON means the same semantic event always yields the same hash.
- Merkle roots let you prove inclusion later without storing everything on-chain.
Step 2: Create a “TEE attestation” object (mocked but cryptographically correct)
Because I couldn’t run hardware attestation in a plain environment, I mocked the attestation as:
- a
measurementhash (think “what code/config is running inside the enclave”) - a
noncethat binds the attestation to this particular provenance bundle - a signature over
(measurement || nonce)
I used an ordinary Ed25519 signature for the demo. Real TEEs use their vendor attestation keys and signed quote formats, but the verification logic pattern stays similar.
import hashlib import json import os from dataclasses import dataclass from typing import Dict from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives import serialization def sha256(data: bytes) -> bytes: return hashlib.sha256(data).digest() @dataclass class MockAttestation: measurement: bytes nonce: bytes signature: bytes public_key_bytes: bytes # for verification in this demo def build_measurement(app_id: str, config_fingerprint: str) -> bytes: # In reality this comes from the TEE measurement mechanism. return sha256(f"{app_id}|{config_fingerprint}".encode("utf-8")) def sign_attestation(priv: Ed25519PrivateKey, measurement: bytes, nonce: bytes, public_key_bytes: bytes) -> MockAttestation: msg = measurement + nonce sig = priv.sign(msg) return MockAttestation( measurement=measurement, nonce=nonce, signature=sig, public_key_bytes=public_key_bytes ) def verify_attestation(att: MockAttestation) -> None: pub = Ed25519PublicKey.from_public_bytes(att.public_key_bytes) msg = att.measurement + att.nonce pub.verify(att.signature, msg) # raises if invalid if __name__ == "__main__": # Demo keys (in real TEEs, the attestation key is hardware-managed). priv = Ed25519PrivateKey.generate() pub = priv.public_key() pub_bytes = pub.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw ) nonce = os.urandom(32) measurement = build_measurement(app_id="node-metadata-prover-v1", config_fingerprint="cfg_9a12") att = sign_attestation(priv, measurement, nonce, pub_bytes) verify_attestation(att) print("attestation verified OK") print("measurement(hex) =", att.measurement.hex()) print("nonce(hex) =", att.nonce.hex())
Why binding to a nonce matters
Without a nonce, a captured attestation could potentially be replayed. A nonce makes the proof “fresh” for this exact provenance bundle.
Step 3: Compute the on-chain anchor hash (“bundle hash”)
Now I combine:
attestation.signatureattestation.measurementattestation.noncelog_root
into a single bundle_hash that can be recorded on-chain.
import hashlib def sha256(data: bytes) -> bytes: return hashlib.sha256(data).digest() def bundle_hash(att_signature: bytes, measurement: bytes, nonce: bytes, log_root: bytes) -> bytes: # Keep ordering explicit so it’s stable across implementations. return sha256(att_signature + measurement + nonce + log_root) if __name__ == "__main__": # Dummy values (replace with real computed ones). att_signature = b"a"*64 measurement = b"m"*32 nonce = b"n"*32 log_root = b"r"*32 bh = bundle_hash(att_signature, measurement, nonce, log_root) print("bundle_hash(hex) =", bh.hex())
Step 4: Anchor bundle_hash in a minimal smart contract
I wrote a small Solidity contract that stores bundle hashes keyed by an incrementing index.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract ProvenanceAnchor { struct Record { bytes32 bundleHash; bytes32 logRoot; uint256 timestamp; } Record[] public records; event Anchored(uint256 indexed idx, bytes32 bundleHash, bytes32 logRoot); function record(bytes32 bundleHash, bytes32 logRoot) external returns (uint256 idx) { records.push(Record({ bundleHash: bundleHash, logRoot: logRoot, timestamp: block.timestamp })); idx = records.length - 1; emit Anchored(idx, bundleHash, logRoot); } function getRecord(uint256 idx) external view returns (bytes32 bundleHash, bytes32 logRoot, uint256 timestamp) { Record storage r = records[idx]; return (r.bundleHash, r.logRoot, r.timestamp); } }
What happens when I run the pipeline
- I generate event logs locally (
log.jsonl). - I compute
log_rootfrom canonical hashes. - Inside the TEE (mocked here), I produce an attestation over
(measurement || nonce). - I compute
bundle_hash. - I call
record(bundle_hash, log_root).
Later, anyone can verify:
- the TEE attestation is valid (in real deployments),
- the included events match
log_root, - and that
bundle_hashmatches what was anchored.
Step 5: End-to-end demo script (log -> root -> attest -> anchor hash)
This script ties the previous pieces together into one reproducible run.
import json, os from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization def sha256(data: bytes) -> bytes: import hashlib return hashlib.sha256(data).digest() def canonical_json(obj) -> bytes: return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") def hash_event_line(line: str) -> bytes: return sha256(canonical_json(json.loads(line))) def merkle_root(leaves): if not leaves: return sha256(b"empty") level = leaves[:] while len(level) > 1: nxt = [] for i in range(0, len(level), 2): left = level[i] right = level[i+1] if i+1 < len(level) else level[i] nxt.append(sha256(left + right)) level = nxt return level[0] def build_measurement(app_id: str, config_fingerprint: str) -> bytes: return sha256(f"{app_id}|{config_fingerprint}".encode("utf-8")) def bundle_hash(att_signature, measurement, nonce, log_root) -> bytes: return sha256(att_signature + measurement + nonce + log_root) # --- Create a log.jsonl-like list of lines --- lines = [ json.dumps({"ts": 1710000000, "event": "BOOT", "chain_id": "testnet-1"}, sort_keys=True), json.dumps({"ts": 1710000010, "event": "PEERS", "count": 12}, sort_keys=True), json.dumps({"ts": 1710000033, "event": "CONFIG", "fingerprint": "cfg_9a12"}, sort_keys=True), ] # --- Merkle root of event log --- leaves = [hash_event_line(l) for l in lines] log_root = merkle_root(leaves) # --- Mock TEE attestation (Ed25519 signature over measurement+nonce) --- priv = Ed25519PrivateKey.generate() pub = priv.public_key() pub_bytes = pub.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw ) nonce = os.urandom(32) measurement = build_measurement("node-metadata-prover-v1", "cfg_9a12") sig = priv.sign(measurement + nonce) # --- Bundle hash to anchor on-chain --- bh = bundle_hash(sig, measurement, nonce, log_root) print("log_root =", log_root.hex()) print("measurement =", measurement.hex()) print("nonce =", nonce.hex()) print("bundle_hash =", bh.hex()) print("pubkey(raw) =", pub_bytes.hex())
In my local testing, the key invariant was that any change to:
- any event content,
- the measurement inputs,
- or the nonce
would change
bundle_hash, which is exactly what you want for provenance integrity.
Practical note: what “verify later” looks like
In a real system, the on-chain record would be used like this:
- Retrieve the anchored
bundle_hashandlog_root. - Provide:
- the raw event lines (or Merkle inclusion proofs),
- the TEE attestation evidence (quote/attestation report),
- and the nonce/measurement.
- A verifier recomputes:
log_rootfrom events (or uses Merkle proofs),- checks the attestation signature/quote validity,
- recomputes
bundle_hash, - confirms it matches what the contract stored.
The important part I learned: the chain stores only small fixed-size digests, while the heavy lifting (logs, proofs, attestation evidence) stays off-chain.
Conclusion
I built a TEE attested log proof pipeline for blockchain node metadata by hashing deterministic JSON events into a Merkle root, binding that root to a nonce inside a measured execution environment (TEE attestation shape), and anchoring the resulting bundle_hash in a minimal smart contract. The big takeaway from my weekend of tinkering is that you can get strong digital provenance beyond “trust the operator” by separating concerns: hardware-backed attestation + off-chain content integrity (Merkle roots) + on-chain digest anchoring.