An event is the chain's account of what happened; an extrinsic is the request that caused it, and carries what no event does — who signed, what they called, the nonce and tip, and whether it worked. A history built from events alone omits every failed attempt and never names a submitter. The same oracle does both. The metadata's extrinsic type carries four parameters and the registry names all of them, including `qp_dilithium_crypto::types::DilithiumSignatureScheme` — the part that looked like it would need hand-written post-quantum knowledge and does not. The chain describes its own signature scheme, so `decode_extrinsic` reads a Quantus transaction without a line of code that knows Quantus exists. Two facts about this chain's extrinsics, both now in CLAUDE.md. The first byte is not the version: the top two bits are a type tag, and mainnet carries `0x84` (signed, v4) and `0x05` (bare, v5) in the same block while the metadata declares version 4 — check the byte against the metadata and you reject every timestamp inherent on the chain. And a signature is 5.3 KiB, two orders of magnitude larger than the call it authorises; it is decoded to find where the call begins and then discarded, keeping only the scheme's name and the byte count. Both halves of a block index in one pass, off calls already being made. Whether a dispatch succeeded comes from the `ExtrinsicSuccess`/`ExtrinsicFailed` events in that same decoded list — read, used, not stored. The block page grows a table below the facts: call, signer, arguments, and the signature's size. The account page merges extrinsics into the history it already had, ordered by block with the request above the results it caused. `source` is 1 for an extrinsic and 0 for an event so that every part of the sort key descends, which lets the paging cursor be a plain row comparison rather than a mixed-direction one Postgres cannot express. Nested calls are why this had to be generic. A `Utility::batch_all` carries whole calls in its arguments, so a transfer's recipient can be three levels down — and because the decoder collects account ids wherever they appear, that batched transfer shows on the recipient's page tagged *received* even though they signed nothing. Verified on mainnet 12,616: the batch renders both its transfers with destinations and values, above the two `Balances::Transfer` events they produced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
62 lines
3.3 KiB
SQL
62 lines
3.3 KiB
SQL
-- What people asked the chain to do, as opposed to what it then reported.
|
|
--
|
|
-- ## Why this is a second table rather than more rows in `chain_event`
|
|
--
|
|
-- An event is the chain's account of what happened; an extrinsic is the
|
|
-- request that caused it. They answer different questions and have different
|
|
-- shapes — an extrinsic has a signer, a nonce, a tip and a fee, and an event
|
|
-- has none of those — so folding them together would mean a table where half
|
|
-- the columns are null for half the rows.
|
|
--
|
|
-- They share the parts that make a per-account view work: `accounts` filled by
|
|
-- the decoder, and a `jsonb` payload in the runtime's own vocabulary. That is
|
|
-- what lets one query merge both into a single history.
|
|
create table chain_extrinsic (
|
|
chain text not null references chain (id) on delete cascade,
|
|
height bigint not null,
|
|
-- Position in the block's body. With the height it is the natural key, it
|
|
-- makes re-indexing idempotent, and it is what an event's `ApplyExtrinsic`
|
|
-- phase points at — the join between the two tables.
|
|
extrinsic_index integer not null,
|
|
pallet text not null,
|
|
call text not null,
|
|
-- Null for an inherent: the block author inserted it and nobody signed it.
|
|
signer text,
|
|
-- The signature *scheme*, by name, never the signature.
|
|
--
|
|
-- A Dilithium signature is 5.3 KiB. At a block a second that is gigabytes a
|
|
-- month of bytes no page renders and no query reads. It is decoded during
|
|
-- indexing because that is the only way to find where the call begins, and
|
|
-- then thrown away; the size is kept because it is the single most
|
|
-- surprising fact about a transaction on this chain.
|
|
signature text,
|
|
signature_bytes integer not null default 0,
|
|
-- Call arguments in the runtime's own names. Nested: a `Utility::batch_all`
|
|
-- carries entire calls in here, which is why an account can be the
|
|
-- recipient of a transfer that is not its own top-level extrinsic.
|
|
args jsonb not null,
|
|
-- Mortality, nonce, tip — the signed extensions as submitted. Null for an
|
|
-- inherent, which signs nothing.
|
|
extra jsonb,
|
|
-- Whether the dispatch succeeded, from the `System::ExtrinsicSuccess` or
|
|
-- `ExtrinsicFailed` event with this extrinsic's index. Read from the events
|
|
-- already decoded for the block rather than stored as rows of their own —
|
|
-- two per block, every block, for one boolean.
|
|
success boolean,
|
|
accounts text[] not null default '{}',
|
|
at timestamptz,
|
|
primary key (chain, height, extrinsic_index)
|
|
);
|
|
|
|
-- "Everything involving this account", the same shape as the event index, so
|
|
-- the merged history is two index scans rather than a scan of either table.
|
|
create index chain_extrinsic_accounts_idx on chain_extrinsic using gin (accounts);
|
|
|
|
-- "This account's activity, newest first", and "this block's extrinsics".
|
|
create index chain_extrinsic_recent_idx
|
|
on chain_extrinsic (chain, height desc, extrinsic_index desc);
|
|
|
|
-- "Every transfer", "every batch" — the call-kind feed, matching the event
|
|
-- index's equivalent.
|
|
create index chain_extrinsic_kind_idx on chain_extrinsic (chain, pallet, call, height desc);
|