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:
- 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).
- 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
Version reviewed:
0.13.4Source:
dist/plugin.js(register-> SQLdigest/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 asingle-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 ofthis 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:
encoding of them e.g. per argument
tag(null|value) ‖ varint(len) ‖ bytesso distinct tuples never collide and null is distinguishable from empty.
MerkleRootprimitive lands, let's have it too reuse the same framingfor leaf/node hashing so tree and scalar commitments are consistent.
This is explicitly an enhancement, not a bug fix as the existing single-value
digestis fine and would remain. It is also fine for the plugin to declinethis and keep field-framing entirely caller-side; in that case a short doc note
("
digesthashes one blob; callers are responsible for canonical, injectiveframing 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 columnsCid(VoteTorrent has ~29 such columns) are therefore not storing IPFS/IPLDCIDs: 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:
Desirable shape:
cid(data, …)/cid_v1(...)wrapping a digest in a multihash + CIDv1, withselectable multicodec (e.g.
raw,dag-cbor), multihash code(sha2-256 / sha2-512 / blake3), and
multibase (default
base32). Input is a single blob, same contract asdigestframing of multiple fields stays the caller's job.cid_decode-> version / codec / hash code / digest) soschemas can validate and migrate.
This lets downstreams (per
votetorrent/doc/schema-conventions.md, Option B)turn
Cidcolumns into real, interoperable, upgrade-safe content addressesinstead of bare digests, ideally decided before more signed records exist.
3. A
MerkleRootprimitive for per-attribute selective disclosureWhy
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'ssignature), 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 thewhole pre-image -> all-or-nothing). The Merkle tree
over per-attribute salted leaves: reveals
(name, value, salt)+ an auditpath per disclosed field; undisclosed fields appear only as opaque sibling
hashes.
Wanting this in the plugin (not purely app-side) is deliberate:
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).
verification) all share one canonical implementation and can't drift on
leaf encoding / tree shape.
Requested API shape (your call)
merkle_root(hexLeafHashes_json): plugin owns only thetree (the two domain-separated hashes + split rule). App owns leaf
construction (salt / flatten / encode). Maximally reusable, but the DB
CidValidCHECK would need leaf hashes materialized, not raw details so itcan't fully enforce the root from the stored record.
selective_merkle_root(selectiveDetails_json): pluginowns the entire canonicalization above. Then
CidValid check (Cid = selective_merkle_root(SelectiveDetails))is a one-linerand 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)).
merkle_verify_inclusion(root, leafEntry, proof_json): so theone canonical impl also backs recipient-side verification (no drift).
Recommendation: the selective-specific shape (or the generic
merkle_rootplus 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
votetorrent-digest-canonicalization.mdvotetorrent/doc/schema-conventions.md: "Cid columns are digests, not CIDs"