Skip to content

solvi.signature

A signature of a trace or a store (64 bytes by default) that names the one changed record and restores its content hash (preview).

A signature of a trace or a store: a few numbers that, besides saying "changed", say WHICH record changed and what its content hash was.

The hash chain (Trace.replay, TraceStorage.verify) is strict integrity. When a record is edited and every hash after it is recomputed together with the stored head, a head kept elsewhere (verify(anchor=...)) still says "rewritten" — but not where, and not what was there. A signature kept next to that head answers both, for one changed record:

sig = sign(store)                  # {"alg": "syndrome", "count", "root": 2 numbers}; keep it where you keep the head
...
locate(store, sig)                 # None: unchanged; an index: the one record that changed
repair(store, sig)                 # {"index", "digest": its original content hash, "match": the candidate it equals}

Every signed item is reduced to its content hash (SHA-256; the chain fields prev, hash, id left out, so recomputing the chain does not move the other items). The code is named in the signature's "alg":

  • "syndrome": S0 = Σ h_i and S1 = Σ (i+1)·h_i mod a 256-bit prime — 64 bytes. One change at k by d moves S0 by d and S1 by (k+1)·d: k = ΔS1 / ΔS0, and the original hash is h_k − d, all 32 bytes. Several changes give a k outside the store (except with probability ~n / 2^256): detected, not located. A classical single-error-locating code.

The positional octonion code that was the second "alg" up to 0.7 is an experiment in benchmarks/octonion_signature.py since 0.8: on a flat store it located exactly as the syndrome code does, 4x larger and ~60x slower (benchmarks/trace_signature.py compares the two).

Limits (tests/test_signature.py pins each): - one changed record is located and its content hash restored; the record itself (its value) only from candidates (a backup, another replica, the values that fact had elsewhere) — the signature holds a hash, not the record; - several changed records (a reorder is two) are detected but not located: locate raises NotLocatable; - a deleted or inserted record shifts every position after it: detected as a count or content change, not located; - records appended after signing are not covered (sign again or extend(), as with the head); the signed prefix is checked; - it is an error-locating code, not a MAC: someone who can rewrite the signature too can forge it. Keep it where you keep the head.

NotLocatable

Bases: ValueError

The signature does not match, and not because of one changed record (several changed, reordered, deleted or inserted records, or another count).

sign

sign(obj, alg='syndrome')

The signature of a trace, a Response, a TraceStorage (its chained records) or a list of items → {"alg", "count", "root"}: plain JSON — keep it next to the head, in a ticket, a log you do not control.

alg "syndrome" (the only one): root = two numbers mod a 256-bit prime (hex; 64 bytes) — S0 = Σ h_i, S1 = Σ (i+1) h_i over the content hashes; one change at k by d moves them by d and (k+1) d, which gives k and the whole original hash.

extend

extend(signature, new_items, start=None)

The signature after appending new_items to what signature signed, in O(len(new_items)). start: the position of the first new item (default: signature["count"]).

load

load(s)

A signature from JSON text, a file path, or a dict (checked: its "alg" says how to read it).

check

check(obj, signature, candidates=None)

Compare obj with a signature taken earlier (its "alg" says which code) → {"ok", "alg", "count", "signed", "index", "digest", "match", "reason"}.

ok: the first signed items are the ones signed. Otherwise, when one item changed: index (its position; for a trace 0 is the input, i the record i-1), digest (the hex of its original content hash, 32 bytes) and match (the first of candidates — records, or for a trace record also plain values — whose content hash is that digest; None). When it cannot be located: index None and the reason. Items appended after signing are not covered and do not count.

locate

locate(obj, signature)

None when the signed items are unchanged; else the position of the one that changed (for a Trace / Response 0 is the input, i the record i-1; for a store, its seq). NotLocatable when the change is not one item.

repair

repair(obj, signature, candidates=())

The one changed item → {"index", "digest": hex of its original content hash, "match": the candidate that is the original, or None}; None when nothing changed; NotLocatable as locate. A candidate is a record (a stored dict, a trace Record, bytes) — or, for a trace record, its original value.