UpveraTechnology
Guide· For anyone checkingDay Roots

How to verify a hash chain of daily records

Short answer

A hash chain links each record to the one before it by storing the previous record's digest. To verify it, start from the first record you trust, compute each record's digest in order, and check that the next record's "previous" field equals it. If any record was changed, the link to the next record breaks, and hiding that would mean rewriting every record after it. The runner on this page does this against a real public chain in your browser.

Updated
Reading
3 min
The need
Confirm that a published sequence of records has not been edited, reordered or cut since it was first published.
Plain text
/guides/verify-hash-chain.md
Run this check against the real repository
Press Run. Each day file is fetched from GitHub, made canonical, hashed and matched to the next day's prev.

#What a hash chain guarantees

Each record carries the digest of the record before it. Change one byte of an old record and its digest changes, so the next record's link no longer matches. The only way to hide an edit is to rewrite every later record too.

That is why a chain is usually paired with something that pins its latest link in public: a timestamp, a commit in a public repository, or a value other people have already copied. Once the tip is pinned, the history before it can't be rewritten quietly.

#What you need

  • The first trusted value, often called genesis. Every check starts from it.
  • The records, in order, as their exact bytes or as data with a canonical form so the bytes can be rebuilt.
  • The digest rule: which hash function, which bytes, and any prefix.

For Upvera's public chain these are genesis.json, one file per day under days/YYYY/MM/, and the rule sha256("upvera/day/v1\n" + canonical JSON). All three are in upvera-co/ledger-roots.

#The check, in about 20 lines

const RAW = "https://raw.githubusercontent.com/upvera-co/ledger-roots/main";
const sha = async (s) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s)))]
  .map((b) => b.toString(16).padStart(2, "0")).join("");
// day files hold no arrays, so objects and primitives are enough here
const jcs = (v) => v === null || typeof v !== "object" ? JSON.stringify(v)
  : "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + jcs(v[k])).join(",") + "}";

const genesis = await (await fetch(`${RAW}/genesis.json`)).json();
let prev = genesis.hash;
let day = genesis.header.at.slice(0, 10);
for (;;) {
  const res = await fetch(`${RAW}/days/${day.slice(0, 4)}/${day.slice(5, 7)}/${day}.json`);
  if (res.status === 404) break;                     // reached the latest published day
  const text = await res.text();
  const rec = JSON.parse(text);
  if (text !== jcs(rec) + "\n") throw new Error(`${day}: not canonical`);
  if (rec.prev !== prev) throw new Error(`${day}: chain broken`);
  prev = await sha("upvera/day/v1\n" + jcs(rec));
  day = new Date(Date.parse(day) + 86400000).toISOString().slice(0, 10);
}
console.log("chain intact, tip", prev);

Paste it into a browser console, or press Run on this page, which runs the same logic and prints each link.

#Reading the result

  • Every link matches. No published day was edited, removed or reordered after the next one was written.
  • "prev link broken". The file before it changed, or a day was replaced. The first broken link tells you where.
  • "not canonical". The file isn't in the form its digest was computed over. It may have been reformatted, which is enough to fail.
  • It stops early. A missing day ends the walk. Days must be consecutive, so a gap is itself a finding.

#What a chain does not prove

A chain proves the records weren't changed after they were linked. It doesn't prove they were true when written, and it doesn't stop someone from rewriting the whole chain if nobody has a copy of its tip. Pin the tip somewhere independent: OpenTimestamps for the time, a public repository with protected history for the content, and your own saved copy for good measure.

#Questions

Do I need to start from genesis every time?

Only once. After that you can save the latest digest you checked and continue from it next time.

How is this different from a Merkle tree?

A chain orders records in time. A Merkle tree summarizes many records at once and lets you prove one of them is included. Upvera uses both: a tree per day, and a chain across days.

What if the files move to another host?

The check doesn't depend on the host. Any copy of the files gives the same digests, so mirrors are easy to verify.

#How Upvera does it

Upvera seals its recorded events, such as Prime, comments and follows, and its UPV movements into minute blocks, folds each day into one root, and chains the days. The day files hold digests only. They are published to a public GitHub repository whose main branch blocks force pushes and deletions, and each file is timestamped with OpenTimestamps. The same check runs on the Verify page and in the Day Roots report.

#Sources