feat: minted keys, VRF seeds, RFC 9162 proofs, history roots, tag URIs and a transparency log
Keys (spec/02 §2.4), the rule paperstack settled on:
- A work key is a Scholar-style citation key, a hyphen and the first four
Crockford base32 characters of SHA-256("scienting-key/1:" + identity),
where the identity is the first of doi, arxiv, isbn, pmid, pmcid and
openalex in same_as, else the folded title, year and family name. A
collision lengthens the later key by two characters; a key is minted
once and never recomputed. A document key adds .si, .v<seq> or .<type>
for versions other than the version of record.
- Every fixture key is now minted (hale2026harmonic-damb,
okafor2025solvent-acth, writer2027fixed-ynr7.v3, tiny2026tiny-j4qm), so
every node and record ID was re-minted and the docs rewritten to match.
The validator checks the document key against the work key and version
(doc.key.work) and the hash part against the identifier (doc.key.suffix).
vectors/keys.jsonl pins the cases paperstack's tests pin.
Review:
- Spot-check seeds are ECVRF-EDWARDS25519-SHA512-TAI proofs (RFC 9381),
implemented in tools/oracle/vrf.py in plain Python and reproduced
against the RFC's Example 16; the sample ranks by the VRF output. The
earlier Ed25519 seed could be ground by a dishonest checker.
- RFC 9162 inclusion and consistency proofs (crypto.inclusion_path,
verify_inclusion, consistency_proof, verify_consistency) with
scienting-proof/1, vectors/proofs.jsonl and two fixture records.
- document.json carries `history`, the root over the corrections, with
the empty-tree hash for none (spec/11 §11.5, rule doc.history).
- A provisional transparency log (spec/23): log entries, signed-note
checkpoints with key hashes as transparency logs share them, the rule
that a logged revocation beats a self-asserted time, and vectors.
- Reserved signature schemes cose and ml-dsa-65 (spec/08 §8.4).
Identifiers and text:
- urn:scienting: becomes tag:scient.ing,2026: (RFC 4151), since the URN
namespace was never registered.
- Unicode 16.0 is pinned for normalization and categories (spec/01 §1.4);
the checker refuses another version.
Checking:
- check.py: keys, proofs, VRF and checkpoint vectors; the proof and
checkpoint records; a check that every ID and hash the prose quotes
exists in the fixtures or vectors; three more broken packages
(history-wrong, key-suffix, key-work).
- stress.py: key, VRF, proof and checkpoint properties.
- Docs: a keys section in the concepts and FAQ, rationale entries for
keys, VRF, proofs, tag URIs and the Unicode pin, a "Looking ahead"
section, media-type registration and OCI distribution notes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
307 files changed
+2698
-1497
No files matched your search
@@ -70,6 +70,130 @@ def receipt_items(receipt):
|
||||
return items
|
||||
|
||||
|
||||
EMPTY_ROOT = hashlib.sha256(b"").hexdigest()
|
||||
|
||||
|
||||
def history_root(items):
|
||||
"""Root over an append-only history, with RFC 9162's hash of the empty list for no items."""
|
||||
if not items:
|
||||
return EMPTY_ROOT
|
||||
return _mth([_leaf(jcs.dumpb(list(it))) for it in items]).hex()
|
||||
|
||||
|
||||
def leaf_hash(item):
|
||||
return _leaf(jcs.dumpb(list(item))).hex()
|
||||
|
||||
|
||||
# --- RFC 9162 proofs (§2.1.3 and §2.1.4) ---------------------------------------
|
||||
|
||||
|
||||
def _path(m, leaves):
|
||||
"""PATH(m, D[n]): the inclusion path for the leaf at index m, as RFC 9162 §2.1.3.1 defines it."""
|
||||
n = len(leaves)
|
||||
if n == 1:
|
||||
return []
|
||||
k = 1
|
||||
while k * 2 < n:
|
||||
k *= 2
|
||||
if m < k:
|
||||
return _path(m, leaves[:k]) + [_mth(leaves[k:])]
|
||||
return _path(m - k, leaves[k:]) + [_mth(leaves[:k])]
|
||||
|
||||
|
||||
def inclusion_path(items, index):
|
||||
"""The audit path of `items[index]` in the tree over `items`, as hex hashes, leaf-most first."""
|
||||
if not 0 <= index < len(items):
|
||||
raise ValueError("index outside the tree")
|
||||
return [h.hex() for h in _path(index, [_leaf(jcs.dumpb(list(it))) for it in items])]
|
||||
|
||||
|
||||
def verify_inclusion(root, leaf, index, size, path):
|
||||
"""Whether `path` proves that the leaf hash `leaf` is item `index` of a tree of `size` with `root`
|
||||
(RFC 9162 §2.1.3.2). Never raises."""
|
||||
try:
|
||||
if not 0 <= index < size:
|
||||
return False
|
||||
fn, sn = index, size - 1
|
||||
r = bytes.fromhex(leaf)
|
||||
for p in path:
|
||||
p = bytes.fromhex(p)
|
||||
if sn == 0:
|
||||
return False
|
||||
if fn & 1 or fn == sn:
|
||||
r = _node(p, r)
|
||||
while fn & 1 == 0 and fn != 0:
|
||||
fn >>= 1
|
||||
sn >>= 1
|
||||
else:
|
||||
r = _node(r, p)
|
||||
fn >>= 1
|
||||
sn >>= 1
|
||||
return sn == 0 and r.hex() == root
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
def _subproof(m, leaves, b):
|
||||
n = len(leaves)
|
||||
if m == n:
|
||||
return [] if b else [_mth(leaves)]
|
||||
k = 1
|
||||
while k * 2 < n:
|
||||
k *= 2
|
||||
if m <= k:
|
||||
return _subproof(m, leaves[:k], b) + [_mth(leaves[k:])]
|
||||
return _subproof(m - k, leaves[k:], False) + [_mth(leaves[:k])]
|
||||
|
||||
|
||||
def consistency_proof(items, m):
|
||||
"""PROOF(m, D[n]): what shows the tree over the first m items is a prefix of the tree over all n
|
||||
(RFC 9162 §2.1.4.1), as hex hashes; empty when m is 0 or n."""
|
||||
n = len(items)
|
||||
if not 0 <= m <= n:
|
||||
raise ValueError("m outside the tree")
|
||||
if m == 0 or m == n:
|
||||
return []
|
||||
return [h.hex() for h in _subproof(m, [_leaf(jcs.dumpb(list(it))) for it in items], True)]
|
||||
|
||||
|
||||
def verify_consistency(root_m, root_n, m, n, proof):
|
||||
"""Whether `proof` shows the tree of size m with `root_m` is a prefix of the tree of size n with `root_n`
|
||||
(RFC 9162 §2.1.4.2). Never raises."""
|
||||
try:
|
||||
if m == 0:
|
||||
return not proof and root_m == EMPTY_ROOT
|
||||
if m == n:
|
||||
return not proof and root_m == root_n
|
||||
if not 0 < m < n or not proof:
|
||||
return False
|
||||
proof = [bytes.fromhex(p) for p in proof]
|
||||
fn, sn = m - 1, n - 1
|
||||
while fn & 1:
|
||||
fn >>= 1
|
||||
sn >>= 1
|
||||
if fn == 0:
|
||||
fr = sr = bytes.fromhex(root_m)
|
||||
else:
|
||||
fr = sr = proof[0]
|
||||
proof = proof[1:]
|
||||
for c in proof:
|
||||
if sn == 0:
|
||||
return False
|
||||
if fn & 1 or fn == sn:
|
||||
fr = _node(c, fr)
|
||||
sr = _node(c, sr)
|
||||
while fn & 1 == 0 and fn != 0:
|
||||
fn >>= 1
|
||||
sn >>= 1
|
||||
else:
|
||||
sr = _node(sr, c)
|
||||
fn >>= 1
|
||||
sn >>= 1
|
||||
return sn == 0 and fr.hex() == root_m and sr.hex() == root_n
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
# --- Ed25519 (RFC 8032) -------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user