Canon declaration index from transaction scriptSigs - #46
Open
cdonnachie wants to merge 1 commit into
Open
Conversation
A declaration is a signed JSON document a key publishes about refs it
claims — "this ref is my creator profile" — revealed inside a
transaction's scriptSig. That makes this the only scanner in the tree
that reads INPUTS rather than outputs, and nothing already in the DB can
answer these queries: ElectrumX keeps UTXOs, history and headers, while
input scripts survive only inside the reorg window.
So this backfill cannot derive from stored CBOR the way GCM/GMT/GA/GMH
do; it reads blocks back from the daemon. That makes its cost per-BLOCK
while declarations are per-DOCUMENT and rare, which two things bound:
DECLARATION_START_HEIGHT skips everything below the format's activation
(a document cannot exist there), and scan_txid indexes one known
transaction in two daemon calls. Scanning from height 0 is a fallback,
not the expected path — a full re-read to find a handful of documents is
the wrong shape of work.
Only SCHEMA-VALID documents are indexed, and the signature never gates
indexing. sig_valid is a HINT, and it is tri-state on purpose: null means
UNCHECKED, not invalid. coincurve is an optional dependency, so a node
without it must not be able to make a client discard valid declarations —
callers that collapse null into false will do exactly that. Structurally
bad signatures (non-base64, wrong length) are decided before any EC work
and do report false. The message magic is Bitcoin-compatible, confirmed
against Radiant-Core's strMessageMagic rather than assumed.
A row means "this document was revealed on chain at this height", never
that its claim is true: anyone can sign a document about a ref they do
not own, and the signature proves only who authored the document. The
endpoints return pointers and hints so Canon re-verifies and decides for
itself.
Storage is three prefixes in a fresh 'DC' family, none a prefix of
another. Height leads the key tail on both secondary indexes, so
height-ascending ordering falls out of iteration with no sort. doc_hash
is sha256 of the pushed body verbatim, which sidesteps any
canonical-JSON question entirely. "Unique on doc_hash, earliest wins" is
enforced on write: a re-reveal adds its by-ref/by-signer rows but never
overwrites the original record.
Versions 1 and 2 are both read. Unlike the HashMark record — where v2
moved the label between pushes, so a v1 parser would index a signature as
a label — a declaration is JSON keyed by name, so a version that only
adds fields stays readable. The parsed version rides on every row so
consumers can still tell them apart, and a JSON `true` is rejected rather
than passing as version 1 through bool/int equivalence.
A document may declare, revoke, or both: `declares` may be empty when the
top-level `revokes` array is not, and a document that does neither is not
indexed. A revoke carries no kind — it withdraws what was declared rather
than restating it — so it is stored as an empty kind and read back as
null, not as the literal string "None".
Exposed as GET /declarations/by-ref/{ref}, /declarations/by-signer/{addr}
and /declarations/stats, plus POST /declarations/scan/{txid} for targeted
ingest (POST, so the security middleware requires an API key). Defaults
off via DECLARATION_INDEX.
40 tests, including the schema check against a real production document
so the live `ref`, signer, ISO timestamps and action-defaulting are
covered by real bytes: revoke-only and combined documents, the null-kind
round trip, height-ascending ordering, earliest-reveal-wins, reorg
unwind, the bounded backfill starting at the activation height, and the
targeted scan.
Also gives the BlockProcessor test stub the declaration_index attribute,
for the same reason the hashmark one needed it: test_parser_halt_guard
builds its processor with __new__ and lists every overlay index by hand.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Indexes Canon declarations — signed JSON documents a key publishes about refs it claims — revealed inside a transaction's scriptSig.
Why this can't be a metadata backfill
This is the only scanner in the tree that reads inputs rather than outputs, and nothing already in the DB can answer its queries: ElectrumX keeps UTXOs, history and headers, while input scripts survive only inside the reorg window (
write_raw_blockdeletes past it).So unlike the GCM/GMT/GA/GMH backfills, this one reads blocks back from the daemon. That makes its cost per-block while declarations are per-document and rare, which two things bound:
DECLARATION_START_HEIGHTskips everything below the format's activation — a document cannot exist there.POST /declarations/scan/{txid}indexes one known transaction in two daemon calls.Scanning from height 0 is a fallback, not the expected path.
sig_validis tri-state, and that mattersOnly schema-valid documents are indexed; the signature never gates indexing.
sig_validis a hint:true/falsenullcoincurveis an optional dependency, so a node without it must not be able to make a client discard valid declarations — anything collapsingnullintofalsewill do exactly that. Structurally bad signatures (non-base64, wrong length) are decided before any EC work and do reportfalse. The message magic is Bitcoin-compatible, confirmed against Radiant-Core'sstrMessageMagicrather than assumed.A row means "this document was revealed on chain at this height", never that its claim is true — anyone can sign a document about a ref they don't own, and the signature proves only who authored it. The endpoints return pointers and hints so Canon re-verifies and decides for itself.
Storage
Three prefixes in a fresh
DCfamily, none a prefix of another:Height leads both secondary key tails, so height-ascending ordering falls out of iteration with no sort.
doc_hashissha256of the pushed body verbatim, sidestepping any canonical-JSON question. "Unique ondoc_hash, earliest wins" is enforced on write: a re-reveal adds its by-ref/by-signer rows but never overwrites the original record.Versions and revokes
v1 and v2 are both read. Unlike the HashMark record — where v2 moved the label between pushes, so a v1 parser would index a signature as a label — a declaration is JSON keyed by name, so a version that only adds fields stays readable. The parsed version rides on every row so consumers can still tell them apart, and a JSON
trueis rejected rather than passing as version 1 through Python's bool/int equivalence.A document may declare, revoke, or both:
declaresmay be empty when the top-levelrevokesarray isn't, and a document doing neither is not indexed. A revoke carries nokind— it withdraws what was declared rather than restating it — so it's stored as empty and read back asnull, not the literal string"None".API
Defaults off via
DECLARATION_INDEX, given the rescan cost.Tests
40 tests, including the schema check against a real production document, so the live
ref, signer, ISO timestamps and action-defaulting are covered by real bytes rather than only synthetic ones: revoke-only and combined documents, the null-kind round trip, height-ascending ordering, earliest-reveal-wins, reorg unwind, the bounded backfill starting at the activation height, and the targeted scan.pytestwasn't available locally, so these ran through a harness withcbor2/pylrustubbed — the REST routes have not been exercised end-to-end, and thesig_valid: truebranch needs an environment withcoincurveto cover.