PROJECT_STATUS: Core indexer FEATURE-COMPLETE — build → deploy → index → query is multi-tenant, hot-reloadable, and verified end-to-end against real Postgres. Hosted deploy/remove writes have bearer-token auth. API operator endpoints now include /healthz, /health, /status, and /metrics. Remaining public-launch work is operational validation: live hosted testnet indexing soak, published benchmarks, and protected logs streaming.
PHASE_0_STATUS: SKIPPED (ecosystem outreach deferred to a separate track)
PHASE_1_STATUS: COMPLETE (reference docs under docs/references/: KIP-20, Kaspa RPC, The Graph compatibility, BlockDAG reorg semantics, KRC20/KRC721 deep dives)
PHASE_2_STATUS: COMPLETE (2.1 workspace; 2.2 multi-RPC failover client + audit; 2.3 continuous wRPC subscription with capability probe, backoff reconnect, gap-aware + anchor-based recovery, health-probe loop; 2.4 store schema (9 migrations) + KIP-20 covenant-id lineage with fork-edge (parent_utxo) model + reorg-safe unwind; 2.5 detector registry = 17/17 REAL silverc-captured fingerprints (12 OpenSilver core exact + KCC20 asset & 4 controllers anchored) + KRC20/KRC721 inscription parsers & ledgers; 2.6 wasmtime mapping runtime with entity reads (store_get); 2.7 KCC20 operation decoder → live CovenantSpent dispatch; 2.8 POI computed over dispatched entity state + third-party verify)
PHASE_3_STATUS: SUBSTANTIALLY COMPLETE (GraphQL gateway: committedBlock(s) / poiCheckpoints / detectedPatterns / covenantLineage [+ first-class utxo/parentUtxo/childUtxos DAG] / entity / entities / covenantSpends, PLUS typed per-subgraph schemas generated from each subgraph's schema.graphql with relation + @derivedFrom resolution; MCP delegates execute_query/get_schema to the same gateway and is registry-aware; KasStream in-process hub + GraphQL subscriptions over Postgres LISTEN/NOTIFY)
PHASE_4_STATUS: COMPLETE (init / codegen / build / deploy / status / remove / mcp-config; operator inspection now includes health, index status, latest POI, DB stats, and RPC status; logs tail / POI range verify / detailed index inspect remain pending hosted backends)
PHASE_5_STATUS: IN_PROGRESS (deploy pipeline FEATURE-COMPLETE: kasgraph_subgraph registry, HTTP deploy endpoint + `kasgraph deploy --node <url>`, bearer-token auth for deploy/remove writes via KASGRAPH_DEPLOY_TOKEN, wasm-bytes data plane, node consumes the registry at startup, multi-subgraph fan-out, dynamic registry reload, operator endpoints /healthz + /health + /status + /metrics, live soak endpoints /soak/status + /soak/metrics + /soak/events + /soak/logs + /soak/summary, CORS, simple per-IP rate limit; 24h testnet soak completed successfully; REMAINING: protected hosted log backend, hosted API topology, benchmarks)
PHASE_6_STATUS: COMPLETE (six reference subgraphs under examples/: kasbonds, opensilver-patterns, krc20, krc721, network-stats, zk-proofs — each ships subgraph.yaml + schema.graphql + src/mapping.ts (AssemblyScript) + README, exercised by tests/)
PHASE_7_STATUS: NOT_STARTED (integrations)
PHASE_8_STATUS: NOT_STARTED (Toccata-window mainnet launch)
PHASE_9_STATUS: DEFERRED (post-launch roadmap; documented, not executed)
COMPONENTS_LIVE: full build→deploy→index→query pipeline — kasgraph-rpc (failover + continuous wRPC subscription + recovery); kasgraph-store (9 migrations incl. deployed-subgraph registry with wasm bytes; per-subgraph schemas; reorg-safe unwind); kasgraph-detectors (17 REAL fingerprints + KRC20/721 ledgers); kasgraph-mapping (wasmtime runtime, store_get); kasgraph-poi (chain + verify); kasgraph-node (ingest → detect → dispatch → persist → POI, multi-subgraph fan-out + registry hot-reload); @kasgraph/api (GraphQL gateway + typed per-subgraph queries + deploy HTTP endpoint); @kasgraph/mcp (5/8 tools live, registry-aware); @kasgraph/cli (init/codegen/build/deploy/status/remove). Verified end-to-end against real Postgres (tests/e2e-deploy-query.test.ts + Rust integration-pg suites).
TESTNET_INDEXED_BLOCKS: 236759
TESTNET_SOAK_STATUS: COMPLETED_SUCCESS
TESTNET_SOAK_DURATION: 24.0 hours
TESTNET_SOAK_DATE: 2026-06-01
TESTNET_DAA_START: 1
TESTNET_DAA_END: 479501516
TESTNET_POI_CHECKPOINTS: 236759
TESTNET_RESTART_RECOVERY: Not measured
TESTNET_PUBLIC_LOGS: /docs/artifacts/testnet-soak/2026-06-01/
SUBGRAPHS_DEPLOYED: 0 live (deploy pipeline complete + e2e-tested: build → deploy → index → query → reload against real Postgres)
QUERY_LATENCY_P95: N/A (not yet benchmarked under load)
KNOWN_SOAK_ISSUES: KasGraph soak API ran on 127.0.0.1:4002 because 127.0.0.1:4000 was occupied by LiteLLM.; Root cause fixed before completion: local TN10 was stale kaspad v1.1.0 and root disk was too full for the Toccata pruning-point UTXO import; the completed run used kaspad v1.2.1-toc.3 with sufficient disk.; At the 24-hour completion point, kaspad still reported phase syncing live DAG and kaspadSynced false; KasGraph indexing, RPC audit, Postgres, GraphQL health, and POI checkpoints remained active through the completion target.
MCP_TOOLS_LIVE: 5 of 8 (list_subgraphs, get_schema, execute_query, search_by_pattern, get_covenant_lineage; get_address_activity + find_subgraphs_for_address need an address-indexed view, query_natural_language needs an LLM hook)
BLOCKERS: NONE for core logic — the indexer is feature-complete core infrastructure. Do not claim production-readiness until live testnet indexing and benchmark numbers exist.
NEXT_PHASE: Phase 5 operational surface (protected logs streaming + hosted API deployment), benchmark publication, then Phase 7 integrations / Phase 8 mainnet launch.
- Repo initialized with MIT license, comprehensive
README.md,PLAN.mdcopied in,STATUS.md+NEXT_SESSION.md+CONTRIBUTING.mdin place. - Cargo workspace with seven crates per PLAN.md Phase 2.1:
kasgraph-node,kasgraph-rpc,kasgraph-store,kasgraph-mapping,kasgraph-detectors,kasgraph-poi,kasgraph-stream. Each compiles standalone;kasgraph-nodelinks the others. - npm workspace with four packages:
@kasgraph/sdk,@kasgraph/cli,@kasgraph/api,@kasgraph/mcp. Strict TypeScript config; vitest configured. - CI workflow under
.github/workflows/ci.ymlrunsnpm run typecheck+npm test+cargo build --workspace --all-targets+cargo test --workspace. - POI module (
kasgraph-poi) — blake2b-256 hash chain with deterministic-chain regression tests. POI =blake2b-256(prior_poi || canonical_block_bytes). The sorted-canonical encoding is now defined (commit1c992e5, Phase 2.8):canonical_block_bytes(&[CanonicalEntity])sorts entities by(entity_type, entity_id), canonicalizes each JSON state viacanonical_json(recursively sorted keys, compact form — independent of Postgres JSONB key ordering; array order preserved), length-prefixes every field (u32-le, disambiguates field splits), and prefixes the entity count (so empty blocks still hash to a well-defined value). The third-party verify side now exists (commitf7c34d5):verify_poi_chain(&[PoiCheckpoint])recomputes a published chain from genesis ([0u8;32]) and returnsValid{final_poi}orDiverged{index, expected, recomputed}pinpointing the first block whose published POI doesn't match, pluspoi_from_hex(inverse ofpoi_hex) for reading published checkpoints. 17 unit tests. Two honest indexers on the same committed block now produce byte-identical input → verifiable POI chains, and a verifier can replay either one. Node wiring (feed it real dispatched entities instead of the env scaffold) is the Postgres-gated follow-up. - kasgraph-rpc now has a real initial client: primary-first failover, rotating backup order, one-shot health probes, background probe-loop helper, and an in-memory per-block audit log of which endpoint served each block. Regression tests cover failover, backup rotation, health marking, and malformed responses.
- kasgraph-store now has two schema slices: the initial covenant-lineage + POI + RPC audit tables, plus a
kasgraph_committed_block+kasgraph_reorg_auditslice that powers committed-state unwind. The liveStoreexposesrecord_committed_block,unwind_committed_blocks_for_subgraph(single SQL transaction: ordered deletion of POI + audit + committed-block rows followed by a reorg-audit insert), andlatest_poi_for_subgraph(highest-DAA surviving checkpoint, used to re-anchor the in-memory POI hash chain on startup and after unwind). - kasgraph-rpc now exposes a more real Phase 2.3 live-ingestion surface:
ChainNotification, orderedfetch_blocks,recover_blocks_by_hashes, anchor-basedrecover_blocks_in_daa_range(start_hash, from, to)viagetVirtualChainFromBlock, JSONL notification parsing, websocket subscription bootstrap via genericsubscriberequests (BlockAddedplusVirtualChainChanged { include_accepted_transaction_ids: false }), notification-envelope parsing for scaffold, wasm-style, and live upstream wrappers (blockAddedNotification/virtualChainChangedNotificationwith nested scope payloads), point RPC fallback over JSON wRPC forgetBlock/getBlockDagInfo/getVirtualChainFromBlockwhen the endpoint URL isws://orwss://, a live capability probe surface that readsgetServerInfoandgetInfo, explicit rustls provider installation sowss://works in-repo without ad hoc scripts, checked-inexamples/live_wrpc_smoke.rsandexamples/continuous_wrpc_smoke.rs, a newSubscriptionDriverEventside-channel plusspawn_continuous_subscription_with_events(...)for soak-run observability, optional JSON summary output from the continuous smoke example, virtual-chain hash hydration back into fetched blocks, websocket reads that can run unbounded (max_messages = 0) or stop cleanly after a configurable idle timeout, reconnect gap detection that now waits for the first actually-new DAA after reconnect instead of being cleared by stale replay, and fail-fast errors when the remote endpoint explicitly rejects a subscription request. - kasgraph-node now wires the first persistence path end-to-end: with
KASGRAPH_DATABASE_URLset, it connects to Postgres, runs migrations, ensures a subgraph schema, re-anchorsIngestionState.prior_poifrom the highest-DAA surviving POI before ingestion starts (so a restart resumes the same hash chain), prefers parsed notification-stream input whenKASGRAPH_NOTIFICATION_JSONLis provided, can also consume websocket notification streams withKASGRAPH_NOTIFICATION_WS_URLplus optionalKASGRAPH_NOTIFICATION_IDLE_TIMEOUT_MS, otherwise builds minimal live-style notifications (BlockAdded,VirtualChainChanged,RecoveryRequired), fetches one or more block hashes throughkasgraph-rpcwhenKASGRAPH_RPC_PRIMARY_URLis configured, performs a live capability probe before continuous-mode subscription startup and now bails early on missinghasMessageId, missinghasNotifyCommand, or unsupportedrpcApiVersion, has dedicated unit coverage for those rejection cases, keeps probabilistic blocks buffered until a finalized block promotes them, rolls back conflicting probabilistic ranges before replay, invokes the committed-state SQL unwind procedure whenVirtualChainChanged.removed_chain_block_hashesmatches already-committed blocks and re-anchors the POI chain from the new survivor after each unwind, derives the highest locally known pre-gap block as a recovery anchor during continuous-mode reconnect gaps, falls back toKASGRAPH_GAP_RECOVERY_BLOCK_HASHESonly when no local anchor exists, computes scaffold POI hashes for committed blocks, and persists POI, RPC audit, and committed-block tracking records. It still falls back to synthetic scaffold data when RPC env is absent. - MCP tool surface (
@kasgraph/mcp) enumerates the eight tools from PLAN.md Phase 3.2 verbatim; a regression test pins the list so docs + code can't drift. - Manifest types (
@kasgraph/sdk) cover all five Kaspa-native data-source kinds (covenant_id,krc20,krc721,address,utxo). A purevalidateManifest(value)(commit5e6315f) checks a parsedsubgraph.yamlagainst that documented contract — required fields, the closedkind/networkenums, and the{kind:'typescript', file, entities[], handlers[]}mapping shape — returning one{path, message}issue per problem (empty array = valid). It is wired intokasgraph buildright after the YAML parse, so a malformed manifest fails with a precise locator and exit 65 (EX_DATAERR) instead of a confusing downstream error; the six shipped example manifests all validate clean (asserted intests/sdk.test.ts). It deliberately does NOT validate kind-specificsourcecontents or file existence (the caller's concern). A regression test (tests/cli.test.ts, commit2760a84) asserts thekasgraph initscaffold'ssubgraph.yamlpassesvalidateManifest, pinning the template to the same contract the build gate enforces so a freshinit→buildcan't fail on the generated manifest. SDK at 16 tests; JS suite at 186. - Phase 1 reference docs all real under
docs/references/:KIP20_COVENANT_ID_QUERIES.md,KASPA_RPC_REFERENCE.md,THEGRAPH_REFERENCE.md,BLOCKDAG_REORG_SEMANTICS.md(KIP-20 finality, virtual-chain reorg surface, ordered Postgres unwind, POI re-anchoring, replay-safety contract), andKRC20_KRC721_REFERENCE.md(legacy Kasplex inscription rules + native KCC20 asset+controller model + native KRC-721 collection/per-NFT shape). - Phase 2.5 detector engine (
kasgraph-detectors) —Fingerprintwith masked state windows, field-named extraction, and per-pattern registry covering 12 OpenSilver core patterns plus 5 KCC20 variants. Placeholder canonical bytes use a0xFE-prefixed discriminator so no real chain script collides; real OpenSilver compiled bytes wire into the registry without touching the engine. Aregistry_schema()function +dump-registrybinary now emit the live registry as JSON ({version, detectors:[{kind, fields:[{name, byteLen}]}]}) — the machine-readable bridge that downstream per-detector payload codegen consumes. Unit tests cover matching, extraction, validation, registry uniqueness, cross-pattern non-collision, and the schema export (coverage, field-shape, JSON round-trip). - Legacy KRC-20 inscription parser (
kasgraph-detectors::krc20, commitebed8bf) — pureparse_krc20_inscription(payload) -> Krc20Parsefor the Kasplex-era{"p":"krc-20","op":...}envelope carried in the transaction payload field (distinct from native KCC20 covenants). Three-way outcome (NotKrc20/Malformed(reason)/Valid) matching the reference doc's silent-ignore vs debug-log-and-drop handling; strict decimal-u64 amounts; lowercase-ASCII tick normalization with raw text preserved. 10 unit tests. Pure core only — payload extraction in the wire model + the ledger acceptance state machine +kasgraph_krc20_legacy_ledgertable are later slices. - Legacy KRC-20 ledger acceptance state machine (
kasgraph-detectors::krc20_ledger, commit83fd12f) — pure (no-Postgres)Krc20Ledger::apply(&inscription, sender) -> ApplyOutcomeover aBTreeMap<tick, TokenState>. Kasplex rules: deploy first-writer-wins; mint requiresamt <= limANDminted + amt <= max(rejected wholesale, never partial); transfer/burn requiresender_balance >= amt; burn decrements both balance andminted(saturating) sosupply == sum(balances). Zero-balance entries pruned on debit.ApplyOutcomeisAccepted|Rejected(reason). 11 unit tests incl. the supply invariant → detectors at 44. Thekasgraph_krc20_legacy_ledgerstore table ((tick, accepting_block_hash, seq), reverse-acceptance-order unwind) + node wiring (sender resolution from first input) are the remaining Postgres-dependent slices. - Legacy KRC-20 ledger journal table (
kasgraph-store, commitd629b02) —kasgraph_krc20_legacy_ledger(new migration20260529120000), the durable journal of accepted Kasplex-era inscription ops behind the pureKrc20Ledger. State is a pure function of the accepted op stream, so the ledger is reconstructed by replaying rows in acceptance order (KRC20_KRC721_REFERENCE.md:54). Keyed globally by(tick, accepting_block_hash, seq)(matching the lineage tables' global keying);subgraphcolumn scopes only reorg unwind;UNIQUE (tx_hash)is the replay idempotency key. Amounts are TEXT decimal strings (KRC-20 amounts are u64, can exceedi64::MAX, so BIGINT would corrupt them).Krc20LegacyOpRecord+record_krc20_legacy_op/krc20_legacy_op_exists/next_krc20_legacy_seq/unwind_krc20_legacy_ledger. 2 SQL-builder tests + migrator bumped to 5 → kasgraph-store at 15. Node wiring (scan payloads, resolve sender, apply, persist + reorg replay) is the remaining Postgres-dependent slice. - Legacy KRC-721 inscription parser + ownership ledger (
kasgraph-detectors::krc721+::krc721_ledger, commit7f30d30) — the NFT parallel to the legacy KRC-20 slice, both pure and storage-agnostic.parse_krc721_inscription(payload) -> Krc721Parsedecodes the Kasplex-era{"p":"krc-721","op":...}payload envelope (deploy=max,mint=id+uri,transfer=id+to,burn=id; strict decimal-u64 for max/id; lowercase-ASCII tick normalization withtick_rawpreserved; three-wayNotKrc721/Malformed(reason)/Validoutcome).Krc721Ledger::apply(&inscription, sender) -> ApplyOutcomere-derives per-token ownership over aBTreeMap<tick, CollectionState>(max,owners: BTreeMap<id, owner>,minted: BTreeSet<id>): deploy first-writer-wins; mint requiresid < maxAND permanent uniqueness (a burned id stays inmintedso it can never be re-minted); transfer/burn require current ownership. 21 unit tests → detectors at 65. Thekasgraph_krc721_legacy_token/_transferstore tables + node wiring (scan payloads, resolve sender, apply, persist + reorg replay) are the remaining Postgres-dependent slices. - Legacy KRC-721 ledger journal table (
kasgraph-store, commit4a35e11) —kasgraph_krc721_legacy_ledger(new migration20260529130000), the durable journal behind the pureKrc721Ledger, NFT parallel ofkasgraph_krc20_legacy_ledger. State is a pure function of the accepted op stream, so per-token ownership is rebuilt by replaying rows in acceptance order; a reorg deletes rows at/above the reorged DAA and re-replays survivors. Records every op (deploy/mint/transfer/burn); keyed globally by(tick, accepting_block_hash, seq);subgraphcolumn scopes only reorg unwind;UNIQUE (tx_hash)is the replay idempotency key.token_id/max_supplyare TEXT decimal strings, not BIGINT (KRC-721 ids/sizes are u64, can exceedi64::MAX).Krc721LegacyOpRecord+record_krc721_legacy_op/krc721_legacy_op_exists/next_krc721_legacy_seq/unwind_krc721_legacy_ledger. 2 SQL-builder tests + migrator bumped to 6 → kasgraph-store at 17. The reference's_token/_transferhead tables are a query-layer projection over this journal (land with the GraphQL/MCP consumer). Node wiring (scan payloads, resolve sender, apply, persist + reorg replay) is the remaining Postgres-dependent slice. - Ledger replay primitive (
kasgraph-detectors, commitd593819) —Krc20Ledger::replayandKrc721Ledger::replayrebuild a ledger from an ordered stream of accepted ops, the pure form of the reorg / startup recovery contract both ledgers already documented. Legacy token state is a pure function of the accepted op stream, so after a reorg unwind deletes a DAA suffix, replaying the surviving journal rows in acceptance order reconstructs the exact in-memory state. Each ledger gains a reorg-survivor property test (build a DAA-tagged stream, drop rows at/above a cutoff to modelunwind_krc*_legacy_ledger, replay survivors, assert the rebuilt state matches the surviving prefix). +4 tests → detectors at 69. Pure — needs no Postgres or address resolution (senders are already-resolved strings in the journaled stream). The node-side glue (fetch ordered rows, reconstruct inscriptions, callreplay) lands with the Postgres+address-gated wiring. - POI chain verifier (
kasgraph-poi, commitf7c34d5) — the crate's module doc states its purpose is to "allow third parties to verify indexer correctness," but only the compute side existed (compute_poi/canonical_block_bytes/poi_hex). Added the verify side a third party actually runs:verify_poi_chain(&[PoiCheckpoint{canonical_entity_bytes, expected_poi}])recomputes the chain from genesis and returnsPoiVerification::Valid{final_poi}when every published POI matches, orDiverged{index, expected, recomputed}pinpointing the first block that doesn't (empty chain →Validat the genesis prior).poi_from_hexdecodes a published hex POI (inverse ofpoi_hex; rejects bad hex / wrong length viaPoiError::InvalidHex). +7 tests → POI at 17. Pure — needs no Postgres; realizes the crate's stated third-party-verification purpose end-to-end (entities →canonical_block_bytes→ chain →verify_poi_chain). - Ordered legacy-ledger replay reads (
kasgraph-store, commit479f23e) — the read side thatKrc20Ledger::replay/Krc721Ledger::replayconsume, previously missing (the journal could be written and unwound but not read back).fetch_krc20_legacy_ops_ordered(subgraph)/fetch_krc721_legacy_ops_ordered(subgraph)return a subgraph's accepted ops asVec<Krc*LegacyOpRecord>ordered by(accepting_daa_score, tick, seq)— a tick's intra-stream order is its monotonicseq, inter-tick order is irrelevant (ticks don't interact), so the order is deterministic and preserves every tick's acceptance order. Two pure SQL builders unit-tested like the other store queries (column-order pinned to the row tuple); a sharedLegacyOpRowtype alias keeps both 12-column reads clippy-clean. 2 SQL-builder tests → kasgraph-store at 19. The node-side record→inscription reconstruction (inverse of the parser) + the startup/reorg call site are the remaining Postgres+address-gated wiring. - Legacy record→inscription reconstruction + replay (
kasgraph-node::legacy_ledger, commita1e6617) — the read-back inverse of the inscription parser, closing the replay loop except for the Postgres call site.krc20_inscription_from_record/krc721_inscription_from_recordmap a journaled op row's columns (op/amount|token_id/recipient/metadata_uri/max_supply/mint_limit) back into theKrc*InscriptionthatKrc*Ledger::replayconsumes;replay_krc20_from_records/replay_krc721_from_recordsfold a fetched row slice (fromfetch_krc*_legacy_ops_ordered) throughreplay— the row'ssenderis the already-resolved op sender, so the rebuild path needs no address resolution. Reconstruction returnsResult<_, LegacyReconstructError>so a row a future bug stored malformed (unknownop, missing or non-decimal numeric column) surfaces an error rather than silently fabricating an op. 11 unit tests (each op reconstructs, tick/tick_raw preserved, u64 beyondi64::MAXround-trips, the three error paths, and end-to-end replay rebuilding KRC-20 balances + KRC-721 ownership from a row stream) → kasgraph-node at 61.#[allow(dead_code)]until the startup/reorg call site (fetch survivors → reconstruct → replay) lands with the Postgres-backed wiring. - Node-level real-Postgres dispatch-loop coverage (
crates/kasgraph-node/src/mapping_host.rs,integration_pg_testsmodule, behind the sameintegration-pgfeature) — verifies the node end of the wasm-dispatch loop thatmain.rsruns per committed block, which had only ever been exercised with an in-memory snapshot and no DB. 2#[sqlx::test]tests run the full sequence against live Postgres:LoadedMapping::loadfrom a real on-diskbuild/manifest.json+ wasm → seedstore_getfromStore::snapshot_entities→dispatch_committed_hits→ persist viaStore::upsert_entity_version→ reorgunwind_entity_versions. The fixture's WAThandleLockemits only on astore_gethit, so the tests prove the Postgres-sourced snapshot steers the guest: seeded entity → emit → persist → reorg restores the prior version; empty snapshot → miss → no emit, nothing persisted. (Inmapping_host.rsnot atests/file because the node is bin-only.) Feature-gated off by default socargo test --workspacestays 223; 63 node tests with the feature on. Run withDATABASE_URL=... cargo test -p kasgraph-node --features integration-pg. - KCC20 operation decoder (pure core), per-UTXO receipt model (
kasgraph-detectors::kcc20_operation) — the covenantoperationthat was the last missingCovenantSpendfield. Reconciled to the actualkcc20.silreference contract after finding KasGraph's registry/reference assumed an aggregatetotal_supply/mint_noncemodel the contract doesn't implement (per user decision: adopt the real model). KCC20 is per-UTXO: each covenant UTXO is a receipt withowner_identifier/identifier_type/amount/is_minter; a spend consumes an input receipt set and produces an output set.classify_kcc20_operation(prev, next)returnsMint(summedamount↑),Burn(↓) — only a minter branch may change the sum perkcc20.silcheckAmounts— and with supply conserved,RotateControllerif the minter branch's controller binding (is_minter+COVENANT_IDowner_identifier) changed, elseTransfer.as_str()emits the exact strings (transfer/mint/burn/rotate_controller)examples/krc20branches on.Kcc20ReceiptState::from_payloadparses the hex field map aKCC20Assethit produces. TheKCC20Assetregistry fields were updated to the per-UTXO layout (owner_identifier/identifier_type/amount/is_minter) andcli/src/detector-schema.tsregenerated;KRC20_KRC721_REFERENCE.mdcorrected. 10 unit tests → detectors at 79; default 237; TS 186; fmt/clippy/typecheck clean. Pure + extraction-agnostic, deliberately not wired: honest classification needs real on-chain state extraction, but the fingerprint registry still carries placeholder bytes. Lands as a pure core ahead of the real-fingerprint sync, the same discipline askrc20_ledger. - Phase 2.8 POI now hashes dispatched entity state (
kasgraph-node) — POI moved out of the pureIngestionStatetransition into the persist loop (apply_and_persist_notification), computed after dispatch over each committed block's dispatchedEntityVersionRecords (mapping_host::canonical_bytes_for_entities→kasgraph_poi::canonical_block_bytes) when a mapping is loaded, via thecommitted_poi_inputselector; the no-mapping path keeps the block's detector-hit canonical bytes byte-for-byte.CommittedBlockWritedropped itspoi_hashfield (the transition is now pure block-promotion), andprior_poiis advanced in the persist loop so POI also chains correctly from the post-reorg re-anchor. Pure helpers unit-tested (order-independence, distinct-state, empty-block, mapping-vs-no-mapping); the 3 transition-level POI tests were rewritten to the pure level. Default 226 + node-unit 64 + store-integration 8 + node-integration 2 green; fmt/clippy clean. - Covenant-lineage + KRC-721 real-Postgres coverage (
crates/kasgraph-store/tests/integration_pg.rs, sameintegration-pgfeature) — extends the store suite to the most intricate untested SQL:covenant_lineage_reorg_unwinds_and_reanchors_headsverifies the atomic 3-stepunwind_covenant_lineage(delete rows ≥ cutoff → drop orphaned heads → re-point each surviving head at its highest-seqsurvivor viaDISTINCT ON) — head re-pointing that no string test can observe;covenant_lineage_population_and_spend_lifecyclecovers head/row population, thecovenant_lineage_row_existsreplay key,covenant_lineage_continues, and the spend record + DAA-scoped unwind;legacy_krc721_journal_seq_exists_fetch_unwindmirrors the KRC-20 journal round-trip for NFTs (incl. a u64 token id overi64::MAX). Writing these surfaced a real schema constraint — thelineage_row.covenant_id → lineage_head.covenant_idFK means heads must be inserted before their rows (matching how the node populates: open the head per hit, then append the row). Store integration suite now 8 tests green against live Postgres. - First real-Postgres
sqlx::testcoverage (crates/kasgraph-store/tests/integration_pg.rs, behind the newintegration-pgcargo feature) — everykasgraph-storequery had until now only been string-tested (SQL builder text / column order /ON CONFLICT); none had run against a live server, so the migrations, per-subgraph schema bootstrap, and reorg-unwind semantics were unverified end to end. 5#[sqlx::test]tests (each on a fresh auto-migrated DB) now verify against real Postgres: (1) all 6 migrations apply + the 9 base tables exist; (2) the entity-version persistence core —ensure_subgraph_schema→upsert_entity_version→latest_entity/snapshot_entities→unwind_entity_versionsreorg drop → idempotent re-apply; (3) covenant-UTXOtrack/lookup/unwind; (4) the legacy-KRC-20 journal — seq allocation, replay guard, a record→fetch_krc20_legacy_ops_orderedround-trip proving the write/read column mappings agree (incl. a u64 overflowingi64), then DAA unwind; (5) POI + committed-block reorg re-anchor viaunwind_committed_blocks_for_subgraph+latest_poi_for_subgraph. Feature-gated off by default socargo test --workspace(and CI, no Postgres) stays green — default build/test/fmt/clippy verified clean in both feature states. Run withDATABASE_URL=... cargo test -p kasgraph-store --features integration-pg. - kasgraph-stream workspace test surface is green again:
slow_subscriber_observes_lagged_error_then_resumesnow drains retained backlog after the initialLaggederror before asserting fresh-event delivery, matching realtokio::broadcastsemantics instead of assuming one lag notification clears all buffered overflow. - Phase 2.6 WASM mapping runtime (
kasgraph-mapping) is real, no longer a stub. wasmtime is the chosen host engine (resolves the wasmtime-vs-wasmer fork). The EngineConfiglocks determinism: fuel metering (bounded execution; runaway handlers tripOutOfFuelinstead of stalling the indexer), Cranelift NaN canonicalization, threads + relaxed-SIMD disabled. Each dispatch runs in a freshStorefor per-block isolation. ABI: guest exportsmemory+kasgraph_alloc(i32)->i32+ onehandler(ptr,len)per manifest handler; guest importskasgraph.log(level,ptr,len)+kasgraph.store_set(ptr,len); the host writes the event JSON ({block:{daaScore,hash},payload}) into guest memory and collects emitted logs +EntityOps.dispatch()classifies failures:OutOfFuel→FuelExhausted, malformed store_set JSON→DecodePayload, missing/mistyped exports→AbiMismatch, else→HandlerTrap. 10 unit tests driven by hand-written WAT fixtures (no AssemblyScript toolchain needed at this layer). - Spend-semantic payload codegen (
@kasgraph/cli) is landed (commit3e32ca5). Oncovenant_idsources,CovenantSpenthandler payloads now type as{ spend: CovenantSpend; state: <stateType> };CovenantLockedkeeps the plain detector-state union (orunknown).CovenantSpendis emitted once and carries only protocol-observable, registry-independent fields (operation/spentValueSompi/successorCovenantId) from the spend tx + KIP-20 lineage tracker — so it stays honest even when the locked covenant's pattern isn't registered (zk-proofs →{ spend: CovenantSpend; state: unknown }). Subgraph-specific amounts stay derived by the mapping. kasgraph build(TS→WASM compiler) is landed (cli/src/build.ts). It resolves the handler names + mapping file(s) fromsubgraph.yaml, generates a thin AssemblyScript entry (build/entry.ts) that supplieskasgraph_alloc(bump allocator via the--runtime stubheap) and re-exports each manifest handler under the runtime's lookup name, then drives the AssemblyScript compiler (asc, a@kasgraph/clidependency) with determinism flags (--optimize --runtime stub --use abort=— the last drops the strayenv.abortimport the runtime linker doesn't provide). The produced wasm is verified against the Phase 2.6 ABI before success:memory+kasgraph_alloc+ every handler must be exported, and no host import outsidekasgraph.{log,store_set}may remain. Error paths return precise exit codes (66 missing manifest/mapping, 65 parse/compile/ABI, 69 missing toolchain). 8 unit tests compile a real AS fixture and assert the export/import shape.- Entity reads in the runtime ABI (
kasgraph-mapping) — added thekasgraph.store_get(ePtr,eLen,idPtr,idLen)->i64host import so a handler can load a previously committed entity. Returns0on a miss; on a hit the host re-enters the guest'skasgraph_alloc, writes the entitydataJSON into guest memory, and returns(ptr<<32)|len.dispatch_with_entities(event, &EntitySnapshot)seeds the read set; 2 new WAT-driven tests (hit echoes the JSON, miss returns zero) bring the crate to 12. @kasgraph/as-mappingAssemblyScript authoring SDK (as-mapping/assembly/index.ts, a fifth npm workspace package) wraps the host ABI in typed helpers so mappings never touch raw pointers:decodeEvent(ptr,len): Event(block daaScore/hash +payload: JSON.Obj|null),log(level,msg),store.get(entity,id): JSON.Obj|null/store.set(entity,id,data), andobjStr/objU64/objBoolfield accessors. JSON viaassemblyscript-json.kasgraph buildresolves it (and its transitive AS deps) by adding every ancestornode_modulesas anasc --pathroot.- All six example mappings ported to AssemblyScript (commit
ba94d0c).examples/{kasbonds,krc20,krc721,network-stats,opensilver-patterns,zk-proofs}/src/mapping.tsare rewritten from async TS pseudo-code into compilable AssemblyScript that targets the runtime ABI through the SDK, implementing the lifecycle logic each pseudo-code described (entity create/update, branch onspend.operation/successorCovenantId, supply + counter accumulation viastore.get).tests/examples-build.test.tsrunskasgraph buildon every example and asserts an ABI-valid wasm (exportsmemory+kasgraph_alloc+ each handler; imports onlykasgraph.{log,store_set,store_get}). entity_versionspersistence layer (kasgraph-store, commit7acec60) — the substrate where wasm-mappingEntityOps land, versioned by DAA score so reorgs can unwind them alongside committed blocks.EntityVersionRecord/EntitySnapshotRowvalue types plus fourStoremethods:upsert_entity_version(idempotent on(entity_type, entity_id, block_daa_score)),latest_entity(highest-DAA row — seedsstore_get),snapshot_entities(one row per key viaDISTINCT ON ... ORDER BY block_daa_score DESC),unwind_entity_versions(deletes at or above a reorg cutoff). The per-subgraphentity_versionstable is created byensure_subgraph_schema; schema name is validated, so theformat!-interpolated table-qualified name is injection-safe. 4 SQL-builder unit tests (no DB) → crate at 7. This is the storage dependency for wiring wasm dispatch into thekasgraph-nodeingest loop.- Detector-hit → mapping → entity-version bridge (
kasgraph-node::mapping_host, commit9c8aa2e) — the pure, deterministic core of node-side wasm dispatch, landed as a complete unit ahead of the side-effecting glue.locked_mapping_event(hit, daa, hash, handler)builds a typedMappingEventfrom a lock-time detector hit;entity_versions(outcome, subgraph, daa)converts emittedEntityOps into DAA-stampedEntityVersionRecords;dispatch_locked_hit(...)runs a compiledMappingRuntimeagainst a hit, seedingstore_getfrom the committedEntitySnapshot, and returns the records + raw outcome (dispatch errors propagate). 4 unit tests incl. a WAT-driven end-to-end proving snapshot seeding reaches the guest and the op flows back as a record → node at 38. Markedallow(dead_code): the committed-block loop calls it once per-subgraph wasm loading + manifest handler resolution land (the remaining wiring, which needs config shape + Postgres to verify end-to-end). - Build manifest descriptor (
@kasgraph/cli+kasgraph-node, commits27a5a76/130efbe) — the bridge that lets the node learn handler resolution without parsingsubgraph.yaml. The TS CLI stays the sole manifest parser:kasgraph buildnow also writesbuild/manifest.json({name, wasm, dataSources:[{name, kind, patterns, collection, addresses, handlers:[{event, handler}]}]}) alongside the wasm. The node deserializes it withserde_json(no new YAML dependency) viasubgraph_manifest::BuildDescriptor, which exposesload(dir),wasm_path(dir), andresolve_handler(detector_kind, event)— first data source whosepatternsinclude the detector kind, then its handler matching the event. 5 Rust unit tests + 1 CLI test pin the descriptor shape. - WASM dispatch wired into the node ingest loop (
kasgraph-node, commits429dfee/d52614d) —mapping_host::LoadedMapping::load(subgraph, dir)reads the descriptor, resolves + compiles the wasm into aMappingRuntime;dispatch_committed_hits(daa, hash, hits, snapshot)resolves each committed detector hit's handler via the descriptor (format!("{:?}", hit.kind)→resolve_handler(kind, CovenantLocked)), dispatches matched hits seedingstore_getfrom the committedEntitySnapshot, and returns DAA-versionedEntityVersionRecords (unmatched hits skipped, trapping handlers logged + skipped so one bad mapping can't stall the indexer). The committed-writes loop builds a per-block snapshot viaStore::snapshot_entities, persists each emitted record withupsert_entity_version, and the reorg path unwinds entity versions at the committed-unwind cutoff. The whole path is gated onKASGRAPH_SUBGRAPH_DIR: unset → exact prior node behavior (no dispatch).cargo test --workspacegreen (node at 46). Verified by unit tests + WAT fixtures; end-to-end against real Postgres still pending. - Spend-dispatch core (
kasgraph-node::mapping_host, commit412de54) — the pure, deterministic core for dispatching covenant spends, landed symmetric to the locked path and ahead of the input-scanning wire change that feeds it.EVENT_COVENANT_SPENTmanifest event (the locked covenant's detector kind resolves the spend handler);CovenantSpendenvelope (operation/spentValueSompi/successorCovenantId, serde camelCase to match the CLI codegen'sCovenantSpendinterface — protocol-observable fields only, subgraph quantities stay mapping-derived);spend_mapping_event(spend, prior_state, …)builds the{ spend, state }payload codegen types for spend handlers;dispatch_spend_hit(…)runs a compiled mapping for a spend, seedingstore_getfrom the committed snapshot and returning DAA-versioned records (mirrorsdispatch_locked_hit). 3 unit tests → node at 49, warning-clean. Wiring it end-to-end needs transaction inputs in the wire model + a covenant-UTXO tracker. - Transaction inputs in the wire model (
kasgraph-rpc, commitd9ec50c) — the first foundational slice toward wiring spend dispatch.IngestedTransactionInput(spending_tx_hash+previous_tx_hash+previous_output_index) + aninputsfield onIngestedBlock, populated by a resilientextract_transaction_inputswalking each tx'sinputs[].previousOutpoint(an input without a parseable outpoint is skipped; coinbase zero-hash inputs kept but inert). The node can now match a block input's consumed outpoint against a tracked covenant UTXO. 3 parse tests → kasgraph-rpc at 30.block_to_rpcsets inputs empty for now (BootstrapBlockdoesn't carry inputs yet). - Covenant-UTXO tracker (
kasgraph-store, commit4e823d0) — the store-side lookup spend detection needs. Per-subgraphcovenant_utxostable (PK(tx_hash, output_index);block_daa_score,detector_kind,covenant_id,locked_stateJSONB) created byensure_subgraph_schema.CovenantUtxoRecord/CovenantUtxoMatchvalue types + threeStoremethods:track_covenant_utxo(upsert on outpoint, idempotent for recovery replay),lookup_covenant_utxo(subgraph, tx_hash, output_index)(returns the matched lock's kind/covenant_id/locked_state),unwind_covenant_utxos(subgraph, from_daa)(reorg cleanup). SQL builders interpolate the validatedSubgraphIdschema name (injection-safe). 3 SQL-builder unit tests → kasgraph-store at 10. - Covenant-id + lineage assignment subsystem (
kasgraph-detectors+kasgraph-node+kasgraph-store, commitsff9d26a/4ea7fc6/edcb6b7) — Kaspa RPC doesn't expose covenant ids and the KIP-20 spec delegates the lineage model to the indexer (KASPA_RPC_REFERENCE.md:443), so KasGraph computes them. (1)kasgraph_detectors::genesis_covenant_id(tx, output)= domain-separated, versioned blake2b-256 over the genesis outpoint, hex-encoded; established once at genesis and inherited unchanged across the lineage (4 unit tests → detectors at 23). (2) The node classifies each detector hit genesis-vs-transition: a hit whose transaction consumes a tracked covenant UTXO inherits that predecessor's id, else it's a genesis with a fresh id; the assigned id flows to both the detected-pattern row and the tracked UTXO (the previously-Nonecovenant_id is now real). Gated on a loaded mapping (no-mapping path unchanged, no extra lookups). (3)successorCovenantIdis now resolved on detected spends: since transitions inherit the predecessor's id, the successor equals the spent covenant's id when the spending tx produced a tracked same-id covenant output (viaStore::covenant_lineage_continues, an EXISTS check), elseNone(lineage terminates). TheCovenantSpendenvelope is now complete exceptoperation. 143 workspace tests green. The genesis-vs-transition path needs Postgres to exercise end-to-end. - Detected covenant spends persisted (
kasgraph-store+kasgraph-node, commit50869a4) — a detected spend was only logged; now it lands in a per-subgraphcovenant_spendstable (PK(spending_tx_hash, previous_tx_hash, previous_output_index);block_daa_score,detector_kind,covenant_id,spent_value_sompi). Every column is protocol-observable at detection time, so the row is honest today;operation/successorCovenantIdstay absent until a spend-tx decoder can derive them.CovenantSpendRecordvalue type +record_covenant_spend(idempotent on the spending input for replay) +unwind_covenant_spends. Keyed on the spending block's DAA, unwound on reorg independently of the lock-timecovenant_utxosrecord — so dropping a spend block restores spend-detectability for its outpoints while the earlier UTXO row survives. 2 SQL-builder unit tests → kasgraph-store at 12; 139 workspace tests green. - Spent value captured in the covenant-UTXO tracker (
kasgraph-store+kasgraph-node, commit2dcf7b6) — the locked output's value is protocol-observable at lock time, so recording it lets a detected spend honestly reportspentValueSompiwithout the spend-tx decoder.value_sompi BIGINTadded to thecovenant_utxostable,CovenantUtxoRecord, andCovenantUtxoMatch; the node derives it from the locking block's matching(tx_hash, output_index)output and surfaces it on the spend-detection log. This is the oneCovenantSpendenvelope field that needs no spend-tx decoding, so it lands ahead ofoperation/successorCovenantId. 137 workspace tests green. - Covenant-UTXO tracker wired into the node ingest loop (
kasgraph-node, commit6930ebd) — threadsinputsthroughBootstrapBlock/block_from_rpc/block_to_rpcso previous-outpoint data reaches the node. On eachCovenantLockeddetector hit (gated onKASGRAPH_SUBGRAPH_DIRviamapping.is_some()), persists aCovenantUtxoRecord(locked_state = the hit payload) so the locked outpoint becomes spend-detectable. After applying a block, scans its inputs against the tracker vialookup_covenant_utxo— placed outside the!hits.is_empty()guard so a spend in a block that locks no new covenant is still caught — and on a match emits an honestinfo!log. ActualCovenantSpentdispatch stays deferred: the spend envelope'soperation/successorCovenantId/ value can't be honestly populated without a spend-tx decoder, and the example mappings branch onspend.operation, so feeding a placeholder would corrupt them; thedispatch_spend_hitcore (412de54) stays ready/dead_code. Reorg path unwindscovenant_utxosalongside entity versions. +1 threading assertion → node at 49 (137 workspace tests green). Same commit appliescargo fmtcleanup to previously-committed rpc/store/mapping_host code.
- Phase 0 ecosystem coordination. User explicitly skipped this for now. Outreach to Kaspa Foundation, Kasplex, kas.fyi, krc721.stream maintainers, Michael Sutton, Hans Moog, wallet teams remains a launch-day prerequisite per PLAN.md, but does not block implementation.
- Hosted API infrastructure decisions (Phase 5): cloud provider, k8s vs systemd, managed Postgres deploy shape, monitoring provider, and protected log source.
- Phase 2.3 follow-through — Build on the first real live-node validation: one public mainnet endpoint is confirmed reachable (
wss://eric.kaspa.stream/kaspa/mainnet/wrpc/json),getServerInfo/getInfowork there, genericsubscribeacks come back as{"method":"subscribe","params":{"id":...}}, live notifications arrive asblockAddedNotification/virtualChainChangedNotificationwith nested scope payloads, point calls likegetBlock,getBlockDagInfo, andgetVirtualChainFromBlockalso work over that same JSON wRPC socket, continuous mode now validates the advertised capability bits before starting, the checked-in smoke examples have already captured mixed real streams from that node through both one-shot and continuous drivers, reconnect gap detection is better defended against replay-shaped reconnects, and the continuous soak example now emits comparable summary lines plus optional JSON summary artifacts. The first 60-second soak stayed stable at 465 notifications withhighest_daa_seen=444252802,reconnects=0,connections=1, andrecovery_required=0, the first 5-minute soak stayed clean at 2177 total notifications with zero reconnects and zero synthetic recovery requests, and the first 15-minute soak also stayed clean at 5597 total notifications with zero reconnects and zero synthetic recovery requests. Next: use this stability baseline to shift from passive soak validation toward active recovery-path validation — either by adding optional event-log output from the smoke runner or by introducing a controlled reconnect/fault-injection harness aroundspawn_continuous_subscription_with_events(...), then harden the anchor-basedgetVirtualChainFromBlockrecovery path against those traces and keep pushing committed rollback semantics toward production-grade behavior perBLOCKDAG_REORG_SEMANTICS.md. Resolver-based discovery is still noisy from this environment (403/404/523on other candidates), so generalized public-node discovery remains rough even though basic wire validation is no longer blocked. - Phase 2.4/2.8 follow-through — POI-over-dispatched-entities DONE. The node now computes POI in the persist loop (
apply_and_persist_notification) after dispatch, over each committed block's dispatched entity state viamapping_host::canonical_bytes_for_entities+main::committed_poi_input(mapping-loaded) — falling back to the block's detector-hit canonical bytes for the no-mapping path (byte-identical to before). POI moved out of the pure transition (CommittedBlockWritedroppedpoi_hash);prior_poiis now advanced in the persist loop, so POI also correctly chains from the post-unwind re-anchor. Pure helpers unit-tested, and an end-to-end#[sqlx::test](integration_pg_persist) drives the realapply_and_persist_notificationloop: a finalized block with anOpenSilverOwnable-fingerprint output → one committed hit → WAT mapping dispatchesBond/b1→ the persisted POI checkpoint is asserted equal to an independentcompute_poi(genesis, canonical_bytes_for_entities(...))andverify_poi_chain→Valid. Default 227 / node unit 64 / store-integration 8 / node-integration 3 all green. RPC fetch-audit now persisted from the live loop too:MultiRpcClient::drain_audit_log()+main::persist_drained_rpc_auditdrain the client's per-fetch audit (recovery/hydration fetches, failover-aware) each continuous iteration + in the bootstrap arm, making real fetch provenance durable and bounding the previously-unbounded in-memory log; the per-committed-blockserved_byrow still covers subscription-delivered blocks. - Phase 2.5 finisher — real fingerprints need an ANCHORED matcher, not byte-exact sync. Reconnaissance (2026-05-29) established
silverccompileskcc20.siland emitsscript+state_layout {start:1,len:46}(bound-independent; exact per-field offsets mapped — see NEXT_SESSION). But SilverScript unrolls loops into the script, so the ctor'smaxCovIns/maxCovOutschange the fixed bytes wholesale (2728B at 4/4 → 5104B at 8/8). A byte-exact whole-script fingerprint thus matches only one bound combo — so the original "sync compiled bytes into the existing exact matcher" plan is not viable. Path forward: an anchored prefix+suffix matcher — now landed askasgraph_detectors::fingerprint::AnchoredFingerprint(head + tail anchors, variable middle ignored, head-relative masked windows; 5 tests). Confirmed sound: across different patterns the shared head is only ~34B and the shared tail ~2B. The first real fingerprint is landed + verified:kasgraph_detectors::kcc20_asset_fingerprint()— captured from realsilverccompiles ofkcc20.silat bounds (4,4)+(8,8) viaderive_anchored_fingerprint, and a test matches it against a real instance at an unseen bound (5,5) with different state and extracts the receipt fields. That verification caught a real bug — SilverScript storesintlittle-endian on chain, soKcc20ReceiptState::from_payloadnow decodesamountLE. It is now wired into detection:DetectorEntryholds aPatternMatcher(Exact(Fingerprint)|Anchored(AnchoredFingerprint));detect_in_output/registry_schemadispatch through it; KCC20Asset isAnchored(kcc20_asset_fingerprint())— the first registry entry backed by a real compiled-script signature, and the cross-collision test passes with it mixed among the placeholders. Remaining: anxtaskto generalize capture to the other loop-parameterized patterns (per-contract ctor args, programmatic const emission) + the 17-kind cross-collision check, then wireclassify_kcc20_operationinto the node spend path. - Phase 2.8 — Extend the current committed-block write path into the real ingestion loop so every committed block emits a checkpoint.
- Per-detector payload codegen — DONE.
@kasgraph/clicodegen now emits a<Kind>Stateinterface per registeredpattern:selector (covenant-state fields, all hexstring, discriminated ondetectorKind) and types each handler'spayloadas the union of its data source's detector states. Pattern-less sources (krc721collection, utxoaddresses) and unregistered selectors (the ZK-aware family) staypayload: unknown. The detector schema lives incli/src/detector-schema.ts, regenerated fromdump-registryviacli/scripts/gen-detector-schema.mjs. - Phase 2.6 WASM mapping runtime — DONE.
kasgraph-mappingis a real wasmtime host runtime (see "What's done"). Resolves the wasmtime-vs-wasmer fork. The two tracks built on top of it: (a) the TS→WASMkasgraph buildCLI command — compile an AssemblyScript mapping that targets this ABI (memory+kasgraph_alloc+ handlers; importskasgraph.log/kasgraph.store_set) — needs theasctoolchain wired into the CLI; (b) spend-semantic payload fields (operation/amount/newController/toPubkey) — these aren't covenant-state so they aren't registry fields; they're decoded at spend time and belong in the mapping-runtime spend payload, layered onto codegen's event types. - Spend-semantic payload codegen — DONE (commit
3e32ca5).CovenantSpenthandler payloads oncovenant_idsources now type as{ spend: CovenantSpend; state }, with a once-emittedCovenantSpendinterface carrying protocol-observable fields (operation/spentValueSompi/successorCovenantId). Deliberately not including subgraph-derived quantities (amount/balances) — those are computed by the mapping. See "What's done". kasgraph build(TS→WASM compiler) — DONE.cli/src/build.tscompiles an AssemblyScript mapping to an ABI-compliant wasm viaasc, generating thekasgraph_alloc+ handler-re-export entry glue and verifying the output against the Phase 2.6 ABI (see "What's done"). Toolchainassemblyscriptis now a@kasgraph/clidependency.- Port the
examples/mappings to AssemblyScript — DONE. Added entity reads (kasgraph.store_get) to the runtime ABI, shipped the@kasgraph/as-mappingauthoring SDK (event decode + entity store + JSON field accessors over the host ABI), and rewrote all six reference mappings as compilable AssemblyScript.tests/examples-build.test.tscompiles every example to an ABI-valid wasm. The runtime can now dispatch a real subgraph end-to-end. - Wire dispatch into the ingest loop — DONE (commits
27a5a76/130efbe/429dfee/d52614d).kasgraph buildemitsbuild/manifest.json; the node loads it + the compiled wasm (mapping_host::LoadedMapping) and dispatches committed detector hits through the runtime, seedingstore_getfrom committed entity state and persisting emittedEntityOps as DAA-versioned rows, with reorg-time entity-version unwind. Gated onKASGRAPH_SUBGRAPH_DIR(unset → unchanged behavior). See "What's done". - WASM dispatch follow-through — NEXT. The spend-detection scaffolding is now fully wired; what remains is the honest decoder + verification + CLI. (a) the store half of end-to-end Postgres verification now exists (
integration_pg.rs,integration-pgfeature — migrations, entity-version reorg unwind, covenant-UTXO tracking, legacy-KRC-20 journal round-trip, POI re-anchor, all against live Postgres). the node half is now covered too (mapping_host.rs::integration_pg_tests, 2#[sqlx::test]s):LoadedMapping::loadfrom disk →snapshot_entities→dispatch_committed_hits→upsert_entity_version→ reorgunwind_entity_versions, all against live Postgres, with the WAT proving the DB-sourced snapshot drives guest emit/skip. So the lock-time dispatch loop is DB-verified end to end at both layers; (b) the remaining piece to unblockCovenantSpentdispatch — a spend-transaction operation decoder. TheCovenantSpendenvelope is now honestly complete exceptoperation:spentValueSompiis captured at lock time (2dcf7b6),covenant_idis assigned by lineage (4ea7fc6), andsuccessorCovenantIdis resolved from lineage continuation (edcb6b7). Onlyoperation(the covenant's semantic action — transfer/mint/burn/rotate_controller) remains. ✅CovenantSpentdispatch is now LIVE and verified end-to-end (node integrationcommitted_spend_dispatches_covenant_spent): a real KCC20 covenant locked in one block (detected via the real anchored fingerprint), spent in the next, → the node classifies the operation from the consumed+created receipt sets (kcc20_spend_operation), resolves theCovenantSpenthandler,dispatch_spend_hits it, and persists the emitted entities. The spend loop groups inputs by spending tx, gathers consumed (lookup_covenant_utxo) + created (covenant_utxos_created_by_tx) receipts, and only dispatches for homogeneous KCC20 txs (never a guessed operation). The KCC20 operation decoder it builds on (kasgraph_detectors::kcc20_operation::classify_kcc20_operation) uses the actualkcc20.silper-UTXO receipt model (each covenant UTXO = a receipt withowner_identifier/identifier_type/amount/is_minter): the operation is a pure function of the input/output receipt-set delta (summedamount↑ → mint, ↓ → burn; supply held + minter-branch controller binding change → rotate_controller; else transfer), grounded inkcc20.sil'scheckAmountsinvariant, emitting the exact stringsexamples/krc20branches on. It is not yet wired, because honest classification needs the receipt states read out of the on-chain state window and the fingerprint registry still carries placeholder bytes — wiring against placeholder extraction would classify fake state. So the remaining blocker narrows to real state extraction — but reconnaissance found byte-exact fingerprinting is broken by SilverScript loop-unrolling (the ctor'smaxCovIns/maxCovOutsrewrite the fixed bytes), so it requires an anchored prefix+suffix matcher first (stable 64B head with the state masked inside + 154B tail; the state-window offsets are bound-independent —owner_identifier[2..33]/identifier_type[35]/amount[37..44]/is_minter[46]). See item 3 + NEXT_SESSION for the design. (Per-pattern operation semantics for the non-KCC20-asset families — OpenSilver core patterns, ZK family — are still their own follow-ups; the KCC20 asset path is the first real decoder.) The plumbing is all done: spend core (spend_mapping_event/dispatch_spend_hit/CovenantSpend,412de54), transaction inputs in the wire model (d9ec50c), covenant-UTXO tracker store layer (4e823d0), node-side tracking + input-vs-UTXO matching with an honestinfo!log + reorg unwind (6930ebd), and now the operation classifier. Once real state extraction lands, classify the spent vs successorlocked_state, filloperation, build theCovenantSpendenvelope, and swap theinfo!log for adispatch_spend_hitcall; (c) thendeploy/status/logs/removeCLI commands + Phase 5 hosted infra.
| Target | Goal |
|---|---|
| Indexing latency | < 30 s of chain tip at p99 |
| GraphQL p95 | < 200 ms |
| GraphQL p99 | < 500 ms |
| Streaming latency | sub-second |
| Concurrent subgraphs per node | 100+ |
| Uptime during incubation | 99.5% |
- OpenSilver (sibling repo): KasGraph's
kasgraph-detectorscrate will consume OpenSilver's pinned compiled scripts to fingerprint covenant patterns on-chain. The OpenSilver manifest pipeline (artifacts/manifests/) is the source of truth. - KasBonds: First reference subgraph (PLAN.md Phase 6.1). Migrates from custom indexing.