Derived views and the history query tier
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_verifiedmeans 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_assertedmarks 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:
- 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;
- the exporter writes a block's rows and the advanced watermark in one batch, so a height's rows are all visible or none;
- 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; - every read, through a primary key or through a secondary index, is bounded by that same H;
- 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.