Research / Verification

Why a receipt is a hash, not a signature

A receipt shows that a calculation can be redone, not who did it. Why a hash and not a signature, why numbers are hashed as bits, and what a match tells you.

There are two ways to vouch for a number. A signature says that we stand behind it. A hash of the whole record says that you can redo it and see. Every calculation on this site comes with the second, and we call it a receipt.

The choice is deliberate. A signature asks you to trust a key and whoever holds it. A hash asks you to trust arithmetic. Anyone can recompute a receipt on a machine of their own, with tools that they already trust, and the documentation does it in about thirty lines of Python.

What is hashed

A receipt is the SHA-256 of a text. The text is built from the parameters of the rule, such as the risk weights or a correlation matrix written as the regulation prints it, then the inputs, then the results. The layout is fixed. Objects list their keys in sorted order, text is written as a JSON string, there are no spaces, and every number is written as the 16 hexadecimal digits of its IEEE 754 bit pattern after the prefix f64:.

The numbers are the part that people ask about. A printed number loses information, and tools print numbers differently. Here is what JSON, the format in which most programs exchange numbers, writes for five values.

Value JSON writes Bit pattern
0.1 + 0.2 0.30000000000000004 3fd3333333333334
0.3 0.3 3fd3333333333333
−0 0 8000000000000000
Not a number null 7ff8000000000000
1e21 1e+21 444b1ae4d6e2ef50

Negative zero and zero are different numbers that JSON writes the same. A value that is not a number becomes null, and so does infinity. A large number gets an exponent whose style varies between languages. Many spreadsheets show only fifteen significant digits, so they show the first two rows alike. A hash of printed numbers would fail to tell some of these apart and would depend on who printed them. A bit pattern prints the same everywhere, because it is a whole number written in hexadecimal.

The program below shows what that buys. It hashes the first two numbers of the table and counts how many of the 256 bits of the two digests differ.

Python
import hashlib
import struct


def bits(x):
    return struct.pack(">d", x).hex()


def digest(x):
    return hashlib.sha256(("f64:" + bits(x)).encode()).hexdigest()


a, b = 0.3, 0.1 + 0.2
print(bits(a), bits(b))
print(digest(a)[:16], digest(b)[:16])
print(bin(int(digest(a), 16) ^ int(digest(b), 16)).count("1"), "of 256 bits differ")

It prints this.

3fd3333333333333 3fd3333333333334
00c2c42cc573e3da 7f542517bb44ad94
133 of 256 bits differ

The two numbers are neighbours among the doubles, one step apart, and the receipts differ in 133 bits of 256. A receipt cannot be nudged. No small change to a number leaves most of the receipt in place, so a receipt that differs from the right one in only a few bits is a mistyped character and not a nearby calculation.

What a match tells you

A match says that the inputs, under the rules of the build that checked them, give a result with exactly those bits. It does not say that the rules are the regulation. The verification page takes that question statement by statement, and says for each what kind of evidence there is.

What a hash cannot do

  • It does not say who made it. Anyone can compute a receipt for any inputs, including inputs that no bank ever held. A receipt shows that a figure follows from some inputs, not that they are yours.
  • It carries no date. The parameters of the rules are in the text, and the changelog says when each rule changed, but nothing in a receipt says when the calculation ran.
  • It hides little when the inputs are easy to guess. Someone who holds a receipt can try candidate inputs and compare. If the inputs come from a small set, such as a book of one position with a round number, that finds them. Treat the receipt of a small book as you would treat the book.

Where signatures belong

If you need to show who ran a calculation and when, sign the receipt, or have it time-stamped, with a key of your own. It is 32 bytes. A signature says that you stand behind the calculation, and the receipt lets anyone redo it. Each is weak where the other is strong. We do not sign receipts, because our signature would add our word to a check that is meant not to need it.

What we have not checked

  • We assume that SHA-256 resists collisions. We use the implementation that each platform ships, which is Web Crypto in a browser and in workerd, and the standard library in Python. We have compared Python with the kernel on the examples in this essay and in the documentation, and the five engines of the parity run with one another. We have not compared them anywhere else.
  • The layout of the text is written twice, in TypeScript and in the Python of the documentation. A third implementation by someone who did not read ours would be better evidence. That changing any single bit of an input changes the receipt is tested on generated inputs, which is statement KE-02, and not proved.
  • A receipt covers what its payload holds. For the interest rate delta charge that is the book, the three charges and the binding scenario. It does not hold the split of the charge between risk factors, so a match says nothing about that split.

Research

Keep reading

  1. Numerics

    One sum, five engines, one answer

    Five JavaScript engines disagree about the last bit of exp, log, sin and tan. We built the kernel from operations they cannot disagree about, and run all five.

    Tomasz Wierzbicki6 min read

  2. Numerics

    Where a capital number comes from

    A capital charge is a square root, so it does not add up by position. How the engine splits it exactly, and what the page does when it rounds the pieces.

    Marcus Teller and Tomasz Wierzbicki6 min read

The next step

Bring us
one book

A scoping call, a sample book and a parallel run on your data. Most teams have the first reconciled number inside two weeks, and a receipt for it on the day.

Book a demo Work out a price