NakedPnL

The public registry of verified investment performance. Every return sourced from SEC filings, exchange APIs, or platform data.

Registry

  • Registry
  • Market Context
  • How It Works
  • Community

Verification

  • Get Verified
  • Connect Exchange

Legal

  • Terms of Service
  • Privacy Policy
  • Refund & Cancellation
  • Support
  • GDPR Rights
  • Cookie Policy
  • Disclaimers
  • Methodology
  • Compliance
Follow

NakedPnL is a publisher of verified performance data. Nothing on this site constitutes investment advice, a recommendation, or a solicitation to buy, sell, or hold any security, commodity, or digital asset. Past performance does not indicate future results. Trading carries a high risk of total capital loss.

© 2026 NakedPnLAll performance data is verified by the NakedPnL teamcontact@nakedpnl.com
NakedPnL
RegistryPricingHow It WorksCommunitySupport
NakedPnL/Guides/How a SHA-256 Hash Chain Detects Tampering: A Visual Walkthrough
Verification guide

How a SHA-256 Hash Chain Detects Tampering: A Visual Walkthrough

A precise, code-driven walkthrough of how SHA-256 hash chains expose insertion, deletion, and reorder attacks on append-only investment performance records.

By NakedPnL Research·May 7, 2026·13 min read
TL;DR
  • A cryptographic hash chain stores SHA-256(previousChainHash + currentContentHash) on every record, so any insertion, deletion, or reorder propagates a mismatch forward to the chain head.
  • NakedPnL hashes the canonicalised raw API response from each venue (Binance, Bybit, OKX, IBKR), then chains it to the prior NAV snapshot using the literal string "genesis" as the seed.
  • Verification is browser-native: the public verifier at /verify/chain/[handle] re-computes every digest with the Web Crypto API and reports the first index where the chain breaks.
  • Hash chains do not encrypt — they fingerprint. They prove that the data you are looking at is the same data that was published, not that the underlying exchange was honest.
On this page
  1. What SHA-256 actually is
  2. What a hash chain is
  3. Worked example: five NAV snapshots
  4. Why store previousChainHash explicitly
  5. Browser-side verification with the Web Crypto API
  6. What a hash chain does not prove
  7. Where the chain head goes next
  8. Frequently asked questions

A hash chain is the simplest cryptographic structure that turns an ordered list of records into a tamper-evident ledger. Each record carries a fingerprint (a SHA-256 digest) of its own contents and a second fingerprint that mixes in the previous record's fingerprint. Modify any byte at position k and every chained fingerprint from k onwards no longer matches.

NakedPnL uses this structure to publish daily NAV snapshots that any third party can re-verify in their browser, without trusting our database, our cloud provider, or our auditors. This article walks through what the chain is, how it detects the three classes of attack (insertion, deletion, reorder), and exactly which lines of code produce the digests you see on a trader profile.

What SHA-256 actually is

SHA-256 is a member of the SHA-2 family of cryptographic hash functions specified in NIST FIPS 180-4. It maps any byte string up to 2^64 − 1 bits long to a 256-bit (32-byte) output. The function is deterministic, meaning the same input always produces the same output, and it has three properties that matter for hash chains:

  • Preimage resistance: given a 256-bit digest h, finding any input m such that SHA-256(m) = h is computationally infeasible (≈ 2^256 work).
  • Second-preimage resistance: given m1, finding a different m2 such that SHA-256(m1) = SHA-256(m2) is also infeasible.
  • Collision resistance: finding any pair (m1, m2) with the same digest costs ≈ 2^128 operations under the birthday bound — beyond the reach of any current adversary.

Hash functions are not encryption. SHA-256 has no key, the output reveals nothing about the input that you cannot recover by brute force, and it is one-way by design. What it gives you is a 32-byte fingerprint that uniquely identifies a piece of data — change one bit, and on average half of the digest bits flip (the avalanche property).

Why 256 bits is enough
An attacker searching for a SHA-256 collision has to perform ≈ 2^128 operations. At 10^15 hashes per second (the Bitcoin network's order of magnitude), this would take roughly 10^16 years — a billion times the age of the universe. SHA-256 is not the bottleneck in any realistic threat model.

What a hash chain is

A hash chain is an ordered sequence of records r_0, r_1, r_2, …, where each record r_i carries two digests:

  1. contentHash_i = SHA-256(canonicalize(rawData_i)) — a fingerprint of the record's payload alone.
  2. chainHash_i = SHA-256(chainHash_{i-1} + contentHash_i) — a fingerprint that depends on the entire history before it.

For the very first record (r_0), there is no prior chain hash. NakedPnL uses the literal ASCII string "genesis" as the seed: chainHash_0 = SHA-256("genesis" + contentHash_0). This is a deliberate, public, hard-coded constant — the verifier knows it, so anyone can reproduce the genesis chain hash without contacting NakedPnL at all.

import { createHash } from "crypto";

export const GENESIS_SEED = "genesis";

export function contentHash(rawResponse: object): string {
  const canonical = stableStringify(rawResponse);
  return createHash("sha256").update(canonical).digest("hex");
}

export function chainHash(
  previousChainHash: string,
  currentContentHash: string,
): string {
  return createHash("sha256")
    .update(previousChainHash + currentContentHash)
    .digest("hex");
}
Canonical implementation from lib/calculation/audit-hash.ts

Both digests are stored on every NavSnapshot row alongside the trader ID and the snapshot date. The chain head — the most recent chainHash for a given trader — is the single 32-byte value that fixes the entire history.

Worked example: five NAV snapshots

Consider a trader with five daily NAV snapshots from a connected Binance account. To keep the example readable, we'll abbreviate hashes to their first 8 hex characters and represent the canonical raw response as a small JSON document. The real implementation hashes the full venue response.

isnapshotDatenavUsdpreviousChainHashcontentHashchainHash
02026-04-25100000.00genesisa1b2c3d4e5f60718
12026-04-26101200.00e5f607189c8d7e6f11223344
22026-04-27100850.00112233445566aabbccdd0001
32026-04-28102450.00ccdd0001ffee223344556677
42026-04-29103100.00445566778899ccdd00ff11ee
Five chained NAV snapshots — abbreviated digests

The published chain head is 00ff11ee. Now imagine an attacker (a malicious DBA, a compromised cloud account, or a forensic investigator looking the other way) wants to retroactively make day 2 look better — they want to bump 100850.00 to 105000.00.

Attack 1: edit a record in place

Editing the navUsd field on row 2 changes its canonical JSON, which changes contentHash_2 from 5566aabb to (say) 7788eeff. That breaks chainHash_2 = SHA-256(11223344 + 7788eeff), which produces a different digest from the stored ccdd0001. To hide the inconsistency, the attacker must also recompute chainHash_2, then chainHash_3, then chainHash_4 — every chain hash from k onwards.

But the published chain head is 00ff11ee, which has been recorded by external observers (search engine snapshots, allocator screenshots, the daily Merkle root anchored to Bitcoin). To match it, the attacker would need to find some new contentHash_2 such that the recomputed chainHash_4 collides with 00ff11ee. That is a SHA-256 second-preimage attack — infeasible.

Attack 2: delete a record

Deleting row 2 leaves rows 0, 1, 3, 4. Row 3 still references chainHash_2 = ccdd0001 in its previousChainHash field, but row 2 no longer exists. The verifier walking the chain forwards from genesis sees previousChainHash on row 3 that does not match the chainHash of row 1 (which would be 11223344). The chain breaks at index 3.

Attack 3: reorder records

Swapping rows 2 and 3 in the database does not change their stored hashes — but the verifier now sees row 3's previousChainHash (ccdd0001) at sequence position 2, where it expects 11223344. Mismatch detected at index 2.

Three attacks, one signal
Insertion, deletion, and reorder all produce the same observable effect: the recomputed chainHash diverges from the stored chainHash at the first affected index, and the divergence propagates to the chain head. The verifier never has to know which attack happened — only that the chain is broken.

Why store previousChainHash explicitly

An equivalent design would compute previousChainHash on the fly during verification by walking back through the database. NakedPnL stores it as a column on every NavSnapshot row instead, for three reasons:

  • Auditability: a third party fetching a single row sees every cryptographic input it depends on. They do not need to trust that our query joined the right prior row.
  • Resilience: if a row is later deleted or moved cold, the orphaned successor still contains a record of what its predecessor was supposed to be. Forensic reconstruction is possible without database internals.
  • Performance: the verifier reads N rows in one query and validates them in a single forward pass, instead of doing N follow-up reads.
Retention constraint
Because previousChainHash is the literal value of the prior row's chainHash, deleting any row other than the chain head leaves an orphan. The NavSnapshot table is append-only by design. See the retention notes in lib/calculation/audit-hash.ts for the three sanctioned strategies if storage ever becomes an issue.

Browser-side verification with the Web Crypto API

Every modern browser ships a native SHA-256 implementation through window.crypto.subtle.digest. This is what powers the public verifier at /verify/chain/[handle] — the browser fetches the trader's chain via the public read API, then re-computes every digest locally. No server-side trust, no JavaScript dependency on a hashing library, no opportunity for the page to lie about whether the chain validates.

async function sha256Hex(input) {
  const bytes = new TextEncoder().encode(input);
  const digest = await crypto.subtle.digest("SHA-256", bytes);
  return Array.from(new Uint8Array(digest))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

function canonicalize(value) {
  // Deterministic: sort object keys recursively.
  return JSON.stringify(value, (_k, v) => {
    if (v && typeof v === "object" && !Array.isArray(v)) {
      const sorted = {};
      for (const k of Object.keys(v).sort()) sorted[k] = v[k];
      return sorted;
    }
    return v;
  });
}

async function verifyChain(rows) {
  let prev = "genesis";
  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    const ch = await sha256Hex(canonicalize(row.rawResponse));
    if (ch !== row.contentHash) {
      return { ok: false, brokenAt: i, reason: "contentHash mismatch" };
    }
    const linked = await sha256Hex(prev + ch);
    if (linked !== row.chainHash) {
      return { ok: false, brokenAt: i, reason: "chainHash mismatch" };
    }
    prev = row.chainHash;
  }
  return { ok: true, head: prev };
}
Minimal hash-chain verifier using the Web Crypto API

Drop this into any browser console while looking at /verify/chain/[handle], paste the JSON response from /api/chain/[handle], and you will see the same green check the page renders. There is no privileged path.

What a hash chain does not prove

It is worth being precise about the trust model. A SHA-256 hash chain proves that the byte sequence you are reading is the same byte sequence that was published when the chain head was first observed. It does not prove:

  • That the venue (Binance, Bybit, OKX, IBKR) reported true balances. If the exchange itself fabricated the API response, the chain faithfully fingerprints a fabrication.
  • That the trader did not run other accounts elsewhere with offsetting losses. The chain is per-account, not per-person.
  • That the trader is who they say they are. Identity verification is a separate layer (KYC, attestation, optional ID badging).

What it does prove is that NakedPnL has not retroactively altered the published record. That alone closes the most common manipulation vector — selectively erasing bad days after the fact — and it is the necessary foundation on which higher-level trust signals (multi-venue cross-checks, on-chain reconciliation for Polymarket, OpenTimestamps Bitcoin anchors) are built.

Where the chain head goes next

Per-trader chain heads are still trust-on-first-use: an observer who never recorded a chain head before tampering cannot detect tampering. NakedPnL closes that gap by building a daily Merkle tree from every active entity's chain head and submitting the root to the OpenTimestamps protocol, which eventually anchors it to a Bitcoin block. After confirmation, the historical record is locked by Bitcoin's proof-of-work — and any party with access to the public anchor and the chain heads can verify the history end-to-end. Two follow-up articles cover the Merkle construction and the Bitcoin anchoring in detail.

Frequently asked questions

Is collision resistance the same as encryption?
No. SHA-256 has no key and is not reversible. Collision resistance means it is computationally infeasible to find two different inputs with the same digest — about 2^128 operations under the birthday bound. Encryption protects confidentiality; hashing protects integrity. The hash chain proves that data has not changed, not that data is secret.
Why SHA-256 instead of SHA-1 or MD5?
SHA-1 has practical collision attacks (the 2017 SHAttered paper produced two PDFs with identical SHA-1 digests in roughly 2^63 operations). MD5 has been broken since 2004. SHA-256 has no known collision attack better than the generic birthday bound and is the standard used by Bitcoin, TLS, and code-signing across the industry. NIST FIPS 180-4 specifies it for federal use.
What if the exchange itself was hacked or fabricated data?
The hash chain only proves that NakedPnL faithfully recorded what the exchange returned. If the venue served fraudulent balances, the chain fingerprints fraudulent balances. This is why NakedPnL also runs cross-venue reconciliation where possible (for example, comparing Polymarket REST positions against the on-chain subgraph), and why we publish the raw responses themselves so anyone with venue API access can independently re-fetch and compare.
Could someone replace the entire database and recompute all hashes?
Only if no one ever observed the original chain head. NakedPnL publishes the chain head on every public trader profile and badge, anchors a daily Merkle root of all chain heads to Bitcoin via OpenTimestamps, and exposes the chain through a public read API. Once any of those observations exist, replacing the database requires breaking SHA-256 second-preimage resistance — the same assumption that secures Bitcoin transaction integrity.
How long does verification take in the browser?
The Web Crypto API on a modern laptop hashes well over a million SHA-256 operations per second. Verifying a one-year, 365-snapshot chain takes a few hundred milliseconds end to end, dominated by JSON parsing rather than the digests themselves. The /verify/chain/[handle] page renders progress as it walks the chain.

References

  • NIST FIPS 180-4 — Secure Hash Standard
  • MDN — SubtleCrypto.digest()
  • Stevens et al. — The first collision for full SHA-1 (SHAttered)
  • Haber & Stornetta — How to Time-Stamp a Digital Document (1991)
NakedPnL is a publisher of verified investment performance data. We are not an investment adviser, broker, dealer, or asset manager, and nothing on this page constitutes investment advice or a recommendation. See the compliance page for our full regulatory posture.