Derived views and the history query tier

Draft protocolSource at 169cbd2 ↗Source SHA-256 089a8c4…99d

This document is not normative. It describes the derived read surface of a Makechain deployment: the history tables the exporter projects into the store, the query tier over them, the facade that serves authorization-critical reads, and the boundary between unbounded query and bounded proof service. Nothing here enters a block header, and no rule here affects block validity. The document supports specification section 14 (finalized events), section 15 (proofs, light clients, and publication receipts), and section 17 (node roles and the database set). Where it repeats a rule about proofs or roles, the specification is the authority.

1. Scope

Validators and gateways serve no list, search, or pagination queries. Every non-proof read goes to the history query tier. Its block-projected tables are a derived view built only from authenticated sources: the per-block objects, the stored operation-log slices, and the finalized event batches, all re-derivable from certified inputs. Its two ingestion tables are built from verified object bytes and from off-chain signed messages, which are authenticated in their own terms but are not block content, and section 2 keeps them in a separate schema prefix for that reason.

A node exposes only submission, status, proof, sync, and event endpoints. Every other read is a query against this tier.

A derived view must expose the finalized height it reflects.

2. Tables

The query tier is exoware-sql over tables held in the store. Each table is declared with a primary key and with native secondary indexes, which are lexicographic or Z-ordered and may cover columns. A schema prefix holds at most sixteen tables and each table at most fifteen secondary indexes, so a lookup path is a secondary index on the table that owns the rows and never a separate index table.

Rows are immutable. The store appends and never updates in place, it has no delete, and inserting one primary key twice is undefined behaviour it does not detect, so no two rows of one table may share a key. Every key therefore ends in (height, command_index, op_index) or the equivalent version columns of that table, where op_index is the position of the operation inside its command and is zero when a command produces one row for that table. A PUSH that deletes and recreates one ref, a grant and a revoke in two commands of one block, KEY_AUTHORIZE followed by KEY_REVOKE.One, and a claim followed by a release are all legal inside one block, and each of them writes distinct keys under this rule.

One command can yield several rows of credits, so that table fixes its own enumeration rather than inheriting a payload position: the charge first, then the deposit locks, then the deposit discharges, each group in DepositListV1 order, with op_index running from zero over that concatenation. A first import that both credits the ledger and pays the first-import fee enumerates under the same rule. Rebuild-from-mirror equality in section 7 depends on this being fixed, because two projections that ordered these rows differently would disagree on keys while agreeing on content.

Two schema prefixes exist, and they differ in what can be rebuilt.

The block-projected prefix is written only by the exporter, which projects the rows of every table in it inside the block's upload batch, atomically with the block, its certificate, and both operation-log slices. Everything in it derives from block bytes alone, so any party reproduces it from the mirror.

Table Primary key Native secondary indexes Rows
blocks height block hash; timestamp_ms header fields, block hash, certificate presence
commands (height, command_index) intent_digest; (key, generation, sequence) command_id, actor, key, sequence, kind, resource vector, charge
events (height, command_index, event_index) EventId the kind tag and the payload fields of that kind, as the closed registry of encoding contract section 12 fixes them
publications (height, command_index, commitment_index) (project, height, command_index, commitment_index); (key, height, command_index, commitment_index) the occurrence tuple (chain_id, block_hash, command_index, commitment_index) as covered columns, project, commitment, publishing account, key, generation, intent_digest, command_id
memberships (height, command_index, commitment_index) log location project, commitment, log location
refs_history (project, ref, height, command_index, op_index) ref revisions over time
roles_history (project, account, role, height, command_index, op_index) role grants and revocations over time
keys_history (account, key, height, command_index, op_index) delegated key records over time
usernames_history (name, height, command_index, op_index) username records over time
credits (account, height, command_index, op_index) imports, charges, deposit locks and discharges
projects_history (project, height, command_index) (owner, canonical slug, height, command_index) project creation, metadata, controller, slug, status and transfer revisions over time, the status being ACTIVE or FROZEN as R-8.40 fixes it
accounts_history (account, height, command_index) account root, configuration and username-pointer revisions over time
validator_schedule (height, command_index, op_index) scheduled and cancelled validator-set entries
watermark height one row per published block, carrying its block hash and its timestamp; the published height H is the greatest key, and the table is append-only like every other

Fourteen of the sixteen slots of that prefix are used, so its headroom is two tables. A family added later, such as offers, slug claims, a release schedule, or settlement checkpoints, either takes a free slot, folds into one tagged table, or opens a further prefix.

The ingestion prefix is written by the indexer's own object-fetching worker, not by the exporter, because neither object bytes nor off-chain signed messages travel in a block. It is labelled non-reproducible: the mirror does not carry its inputs, so the rebuild-from-mirror equality of section 7 does not cover it.

Table Primary key Native secondary indexes Rows
commits (scheme_id, native_id) one object-identity row per full identity, with metadata derived from verified object bytes
publisher_assertions (commitment, message_id) assertions carried by off-chain signed messages

Apart from that worker the indexer keeps no database and no pipeline of its own: it is a service over the store that answers queries, serves the event subscription and "not found", deduplicates, and assembles command-inclusion proofs and publication receipts from stored blocks. The gateway stays pass-through.

History tables are append-only and carry the height in the primary key. "Current" is a reverse scan to the first row; "as of H" is the same scan bounded by H. Point-in-time state is therefore a query over the history tables, and so is project resolution by slug: a reverse scan of the (owner, canonical slug, height, command_index) index bounded by H returns the latest revision that carried that pair, and the reader checks on that row that the pair is still held. The index matches the uniqueness key (owner, canonical slug) that specification R-8.33 enforces, in the canonical form of encoding contract section 3.3; no normalization happens at read time. Because the tables never update in place, a superseded slug leaves its old row behind by design rather than as stale index residue.

Logical expiry, meaning a record read as absent before it is reclaimed, is projected virtually from the deadline fields the history rows already hold, so the tier never shows expired authority as live. The predicate is evaluated at the consensus timestamp of block H, never at wall-clock time, so two readers at the same H agree. Expiry itself is lazy and is a pure predicate on consensus block time, as specification section 9 states.

3. Publication occurrences and metadata labels

A publication occurrence is (chain_id, block_hash, command_index, commitment_index), as fixed by specification section 15. Every commitment list entry gets its own occurrence, including duplicates within one command. The same object can have many publishers and many publications.

There is no singleton publisher and no singleton publication height on a commits row.

Two metadata labels are used and never merged:

  • object_verified means decoded from bytes whose identity was recomputed under the registered scheme, with the limited meaning fixed by specification section 8. It does not authenticate a claimed author, and it does not establish that an embedded signing key belongs to that author.
  • publisher_asserted marks assertions carried by off-chain signed messages. Such assertions are separate keyed claims referencing the commitment and the message id. They never overwrite object-derived fields, and they never overwrite another publisher's claims.

Publication attribution and order derive from authenticated command bytes, never from object metadata and never from an inferred author.

4. Consistency and pagination

The store offers a monotonic freshness floor rather than snapshot isolation, so the tier supplies its own consistency discipline.

Every query first reads the published watermark height H, which is the greatest key of the watermark table and therefore one reverse scan of one row, then bounds itself to height <= H. The answer is consistent as of H and reports H. That holds only under five conditions, and the design owes all five:

  1. every table of the block-projected prefix carries the height in its key, which is why the two ingestion tables are outside that prefix and outside this guarantee;
  2. the exporter writes a block's rows and the advanced watermark in one batch, so a height's rows are all visible or none;
  3. a query reads the watermark before it reads any table, and carries both H and the sequence number of that watermark read, passed as min_sequence_number, on every later statement, including every page of a keyset scan;
  4. every read, through a primary key or through a secondary index, is bounded by that same H;
  5. the exporter commits block batches strictly in height order, one at a time, so that no height above H is visible while a height at or below H is still missing. Parallelism is admitted only for the qmdb node rows staged ahead of the ordered batch and for the mirror uploads, neither of which can make a later height's rows visible first.

A row for a height above H is never read, so a query that spans several statements sees one cut. The store's own per-log authentication watermark is an operation location, not a height, and is never used for this purpose.

Two watermarks exist and only one bounds queries. The watermark row rides the block's batch, which is what makes condition 2 true, and it states what this store holds. The exporter's own durable publication watermark is a different value: it advances only after the store and the mirror have both accepted the block, and it is what the exporter's recovery and the operators' gap scan use. A query never reads that one.

Pagination is keyset over the primary key or over the index the query uses, carrying the last key seen together with H. OFFSET is never used: its cost grows with the offset, and a growing table makes an offset page unstable. One query returns one response and the store paginates nothing server-side, so the facade caps the page size well inside the response bound rather than letting a caller request an unbounded single response.

5. Subscription

SubscribeFinalized is the Subscribe stream of the events table. The stream emits one frame per store batch that touched that table, and the exporter writes exactly one batch per block, so a frame is one block by construction and frames arrive in ascending height order. Every frame carries its height.

Two consequences follow. A block whose events rows are empty produces no frame, so the consumer, not the stream, synthesizes the empty heights between two frames. A retried upload of one height produces a second frame for it, so a consumer deduplicates by EventId, which is the same idempotence rule specification section 14 states for delivery. A consumer resumes by the sequence it last saw and persists that cursor only together with the effects of the events it covers.

Two retention policies are involved and they are not the same policy. The row families of the events table carry a no-eviction policy, so the rows themselves stay readable. Resuming by sequence number depends instead on the store's sequence-log retention, which is a pinned deployment value and which answers BATCH_EVICTED below its floor. On that answer, and for a consumer that falls behind for any other reason, the fallback is to re-read the missed range by height under the section 4 discipline and deduplicate by EventId.

6. Isolation and scaling

One store serves both the proof service and the query tier at launch. The isolation between them is operational rather than physical: the gateway caches query answers keyed by (query, H), so a repeated question at an unchanged watermark costs the store nothing, and operators alert on store ingestion lag and on exporter publish lag before either reaches its readiness bound.

The scaling step, when measured load requires it, is a second query store written from the same exporter queue. It changes no table, no key, and no answer.

7. Reproducibility

The mirror is the reproducible artifact and the projection is deterministic code. Any party rebuilds the block-projected prefix from the mirror's per-block objects and compares rows.

The two ingestion tables are outside that target, because their inputs are object bytes and off-chain signed messages, which the mirror does not carry. They are rebuilt by re-running the indexer's ingestion worker against the objects and messages it can still fetch, and a party that cannot fetch them rebuilds everything else.

The schema version is part of the table prefix. A schema change is therefore a republish from the mirror into a new prefix, with the old prefix served until consumers have moved. Logical row equality is the reproducibility target, together with the rule that no two rows of one table share a key.

8. The proof-to-claim matrix

This matrix fixes what each claim can be checked against. Specification section 15 carries it in compact form and is the authority for every proof row.

Claim Evidence
These command bytes were finalized at height h command inclusion under command_root, whose leaf is Keccak256(canonical SignedCommand bytes), plus a finality proof
This commitment was published to this project membership inclusion at a stated size, with the leaf recomputed as Keccak256 over project_id, scheme_id, and native_id in that order
This account published this commitment to this project at this ordered position publication receipt: signed PUSH, command inclusion, and finality
This value is current a current-state proof at an authenticated anchor, interpreted with expiry, control generation, and delegation epoch using the relevant parent-state proofs
This value was current at a past height H, such as "ref R of project P pointed at commitment C at H" only a current-state proof captured while H was the head, which stays verifiable against H's certified header forever; not provable retroactively at launch, because a command-inclusion proof at height h cannot show the absence of a later write before H. Specification R-15.6 and section 15.3 carry this row
This derived event or charge is correct bounded replay from certified inputs, or explicitly unauthenticated
This list is complete or latest never provable from row proofs; completeness and freshness are service claims

9. Row validity, completeness, and freshness

Row validity never implies completeness.

A consumer whose decision is authorization-critical, such as an availability layer checking upload authority or a client about to sign, uses verified current state under a stated maximum lag, or declares its trust in its indexer. It never falls back to old history. It knows that a malicious server can conceal newer state, so freshness comes from an independently maintained checkpoint.

Absent is returned only for logically supported absence proofs. The unordered validity-state database grants no general exclusion proof, so the tier's "not found" is a service claim and not an authenticated absence.

10. Facade

Authentication-critical and latency-critical reads are served by the indexer and the gateway from the history tables, behind a small HTTP and SDK facade. Each is the bounded reverse scan of section 2 under the watermark discipline of section 4, never a read of a row that some writer keeps current in place. Project resolution by slug, ref discovery for push, and key state are the reads this facade exists for. A facade answer carries the H it was bounded by and remains a service claim under section 9.

SQL is the history surface. A browser never opens a query engine to discover a ref.

11. Division with the proof service

Unbounded historical questions, such as who changed this ref in a given year, or which commitments a given key published, are query-tier work with no proof-service obligation.

Authenticated historical facts are proof-service proofs under a bounded service objective. Membership inclusion at any location and block-history ancestry are served for every finalized height, but only through the proof-service API and only within the calibrated latency objective. Command inclusion and publication receipts come from the indexer's stored blocks; current-state proofs come from the exporter's live head, and no historical current-state proof is offered. Breadth of history query is unbounded and unauthenticated.

12. Diagnostics stream

The explorer's live view of pre-finalization consensus activity, meaning notarizations, nullifications, and finalizations as they happen, is a non-normative operator surface. Validators and the exporter may expose it through a reporter fan-out.

It carries no protocol guarantee. It is never a source for the query tier. It is not the finalized event feed of specification section 14.

13. Deferred analytics tier

A columnar analytics tier, built later from the mirror, is what the following questions wait for. None of them is on the launch path, and none of them changes a table above.

  • Network-wide aggregates over the whole history rather than a bounded key range.
  • Joins across several history tables at arbitrary heights.
  • Text search over names, slugs, and object metadata.
  • Third-party diffing of an independently built dataset against a published one.

Time-bounded questions are not on that list. The timestamp_ms index on blocks turns a time range into a height range in one bounded lookup, after which the question is an ordinary height-bounded query under section 4.