Skip to content

Feature proposals for quereus-crypto-plugin: a canonical multi-field digest helper, a real CID UDF and a MerkleRoot primitive #5

Description

@risavian

Version reviewed: 0.13.4
Source: dist/plugin.js (register -> SQL digest/sign/verify/hash_mod/random_bytes), dist/index.js (JS exports)
Type: Feature request / enhancement (no bug claimed against the plugin)
Filed: 2026-06-05

Scope

The SQL digest(data, algorithm?, inputEncoding?, outputEncoding?) UDF is a
single-value hash and behaves correctly: it hashes one blob. How an
application combines multiple logical fields into that blob and whether it
does so injectively is the caller's responsibility, exactly as with any
sha256(blob).

VoteTorrent currently rolls its own multi-field digest downstream
and gets the field-framing wrong; that is being fixed on the VoteTorrent side
(companion report votetorrent-digest-canonicalization.md) and is not asked of
this plugin. "Concatenate then hash" is inherently non-injective; that's a
property of concatenation, not a defect in a hash library.

However this situation can be improved with canonical helper functions.

Selective disclosure related feature could also get a help from the crypto plugin.

The 3 additions below would help the downstreams with similar needs.


1. A canonical multi-field / structured digest helper

Several consumers will independently need to hash an ordered tuple of fields,
and each one reinventing the framing is where injectivity bugs creep in
(delimiter joins, null-vs-empty conflation, unencoded arity). The plugin could
optionally offer a vetted canonical encoding so downstreams don't each roll
their own:

  • A SQL UDF that accepts multiple data arguments and hashes an injective
    encoding of them e.g. per argument tag(null|value) ‖ varint(len) ‖ bytes
    so distinct tuples never collide and null is distinguishable from empty.
  • If the proposed MerkleRoot primitive lands, let's have it too reuse the same framing
    for leaf/node hashing so tree and scalar commitments are consistent.

This is explicitly an enhancement, not a bug fix as the existing single-value
digest is fine and would remain. It is also fine for the plugin to decline
this and keep field-framing entirely caller-side; in that case a short doc note
("digest hashes one blob; callers are responsible for canonical, injective
framing of multi-field inputs") would prevent the footgun.

2. A real CID implementation in UDF, ala IPFS' multihash + CIDv1

Today digest() returns a bare hash (base64url of the raw digest bytes)
with no multibase prefix, multicodec content type, multihash
(hashFnCode ‖ length ‖ digest) framing. Consumers that name their columns
Cid (VoteTorrent has ~29 such columns) are therefore not storing IPFS/IPLD
CIDs: the value would not match a CID any content-addressed store computes for
the same bytes, and it carries no algorithm agility (nothing in the value
identifies sha2-256, so the hash function can't be migrated unambiguously).

Request

Add a SQL UDF (and matching JS export) that produces a self-describing
content identifier
:

CIDv1 = multibase( version ‖ multicodec(content-type) ‖ multihash )
        multihash = hashFnCode ‖ digestLength ‖ digestBytes

Desirable shape:

  • cid(data, …) / cid_v1(...) wrapping a digest in a multihash + CIDv1, with
    selectable multicodec (e.g. raw, dag-cbor), multihash code
    (sha2-256 / sha2-512 / blake3), and
    multibase (default base32). Input is a single blob, same contract as
    digest framing of multiple fields stays the caller's job.
  • Round-trip helpers (cid_decode -> version / codec / hash code / digest) so
    schemas can validate and migrate.

This lets downstreams (per votetorrent/doc/schema-conventions.md, Option B)
turn Cid columns into real, interoperable, upgrade-safe content addresses
instead of bare digests, ideally decided before more signed records exist.


3. A MerkleRoot primitive for per-attribute selective disclosure

Why

VoteTorrent voter registration needs per-attribute selective
disclosure
: an authority commits to a registrant's selective-disclosure field
set as a single value (Registrant.SelectiveCid, covered by the authority's
signature), then later reveals a subset of those fields to a permitted audience
with an inclusion proof without leaking the values of the undisclosed
fields. A flat Digest(whole set) can't do this (verifying any field needs the
whole pre-image -> all-or-nothing). The Merkle tree
over per-attribute salted leaves: reveals (name, value, salt) + an audit
path per disclosed field; undisclosed fields appear only as opaque sibling
hashes.

Wanting this in the plugin (not purely app-side) is deliberate:

  1. So the schema can enforce RegistrantSelective.CidValid: Cid = MerkleRoot(…)
    at insert that a forged/incorrect root becomes impossible to store (the same
    "invalid states impossible by design" posture as the rest of the schema).
  2. So the DB, the engine (proof generation), and the recipient (proof
    verification) all share one canonical implementation and can't drift on
    leaf encoding / tree shape.

Requested API shape (your call)

  • Generic, minimal: merkle_root(hexLeafHashes_json): plugin owns only the
    tree (the two domain-separated hashes + split rule). App owns leaf
    construction (salt / flatten / encode). Maximally reusable, but the DB
    CidValid CHECK would need leaf hashes materialized, not raw details so it
    can't fully enforce the root from the stored record.
  • Selective-specific: selective_merkle_root(selectiveDetails_json): plugin
    owns the entire canonicalization above. Then
    CidValid check (Cid = selective_merkle_root(SelectiveDetails)) is a one-liner
    and the root is fully DB-enforced. Heavier and less generic, but it's the
    version that actually closes the invalid-state hole (the motivation in (1)).
  • Companion: merkle_verify_inclusion(root, leafEntry, proof_json): so the
    one canonical impl also backs recipient-side verification (no drift).

Recommendation: the selective-specific shape (or the generic merkle_root
plus a documented canonical leaf-encoding UDF), since DB-side enforcement of the
root from the stored record is the whole point. Either way, the leaf/node framing
should match request 2 so scalar and tree commitments stay consistent.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions