DEV Community

Cover image for How we built a tamper-evident accounting ledger for retail SMBs using SHA-256 hash chaining
sahil salma
sahil salma

Posted on

How we built a tamper-evident accounting ledger for retail SMBs using SHA-256 hash chaining

At Momentum (a product by ltiora) (https://ltiora.com/), we build retail and wholesale ERP software. One of the more interesting engineering problems we tackled was making our accounting ledger tamper-evident, meaning retroactive modification of historical financial records is cryptographically detectable.

The Problem

Most accounting software for SMBs treats historical records as editable data. An administrator with sufficient access can modify a past journal entry, and while most platforms log user actions, those logs are themselves editable. An employee with enough access can modify a transaction and remove the log entry that recorded it.

For retail businesses with multiple staff touching finances, this creates silent audit risk that most operators don't consider until they're in a dispute or undergoing due diligence.

The Solution: Hash Chaining

We applied the same integrity principle used in blockchain systems to each accounting entry.

When a journal entry is written:

  1. We compute SHA-256(entry_data)
  2. The resulting hash is stored in the previous_hash field of the next journal entry
  3. This creates a chain: modifying Entry #N changes its hash, which no longer matches what Entry #N+1 says it should be
interface LedgerEntry {
  id: string;
  sequence_number: number;
  posted_at: string;
  debit_account_id: string;
  credit_account_id: string;
  amount_cents: number;
  description: string;
  previous_entry_hash: string | null; // null for genesis entry
  entry_hash: string; // SHA-256 of all fields above
}

function computeEntryHash(entry: Omit<LedgerEntry, 'entry_hash'>): string {
  const canonical = JSON.stringify({
    id: entry.id,
    sequence_number: entry.sequence_number,
    posted_at: entry.posted_at,
    debit_account_id: entry.debit_account_id,
    credit_account_id: entry.credit_account_id,
    amount_cents: entry.amount_cents,
    description: entry.description,
    previous_entry_hash: entry.previous_entry_hash,
  });
  return createHash('sha256').update(canonical).digest('hex');
}
Enter fullscreen mode Exit fullscreen mode

Verification

We expose a chain verification endpoint that traverses all entries in sequence order and confirms that each entry's previous_entry_hash matches the hash of the preceding entry.

async function verifyLedgerIntegrity(
  tenantId: string
): Promise<{ valid: boolean; broken_at_sequence?: number }> {
  const entries = await db.ledgerEntries
    .where({ tenant_id: tenantId })
    .orderBy('sequence_number', 'asc')
    .all();

  let previousHash: string | null = null;

  for (const entry of entries) {
    if (entry.previous_entry_hash !== previousHash) {
      return { valid: false, broken_at_sequence: entry.sequence_number };
    }
    const expectedHash = computeEntryHash(entry);
    if (entry.entry_hash !== expectedHash) {
      return { valid: false, broken_at_sequence: entry.sequence_number };
    }
    previousHash = entry.entry_hash;
  }

  return { valid: true };
}
Enter fullscreen mode Exit fullscreen mode

Tradeoffs and Edge Cases

Appends only: The hash chain only works for append only ledgers. This maps well to accounting (journal entries are never modified; corrections are made by posting reversing entries), but it means the business logic layer must enforce append only strictly.

Replication: In a multi-region setup, the genesis entry's hash must be agreed upon before any writes. We use a leader election approach for ledger writes to ensure sequence numbers and hashes are assigned deterministically.

Performance: Verification is O(n) in the number of entries. For high-volume retail (millions of transactions/year), we run verification asynchronously on a schedule rather than on every write.

What We Didn't Do

We deliberately didn't implement blockchain style distributed consensus β€” that's overkill for a single tenant accounting system. The threat model here is internal fraud and accidental modification, not a distributed Byzantine failure. A simple hash chain is sufficient.

If this is interesting to you, we published a user facing explanation of the concept here: https://ltiora.com/blog/tamper-evident-accounting-software


Enter fullscreen mode Exit fullscreen mode

Top comments (0)