Skip to content

Latest commit

 

History

History
114 lines (103 loc) · 57.9 KB

File metadata and controls

114 lines (103 loc) · 57.9 KB

KasGraph — Status

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.

What's done

  • Repo initialized with MIT license, comprehensive README.md, PLAN.md copied in, STATUS.md + NEXT_SESSION.md + CONTRIBUTING.md in 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-node links 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.yml runs npm 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 (commit 1c992e5, Phase 2.8): canonical_block_bytes(&[CanonicalEntity]) sorts entities by (entity_type, entity_id), canonicalizes each JSON state via canonical_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 (commit f7c34d5): verify_poi_chain(&[PoiCheckpoint]) recomputes a published chain from genesis ([0u8;32]) and returns Valid{final_poi} or Diverged{index, expected, recomputed} pinpointing the first block whose published POI doesn't match, plus poi_from_hex (inverse of poi_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_audit slice that powers committed-state unwind. The live Store exposes record_committed_block, unwind_committed_blocks_for_subgraph (single SQL transaction: ordered deletion of POI + audit + committed-block rows followed by a reorg-audit insert), and latest_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, ordered fetch_blocks, recover_blocks_by_hashes, anchor-based recover_blocks_in_daa_range(start_hash, from, to) via getVirtualChainFromBlock, JSONL notification parsing, websocket subscription bootstrap via generic subscribe requests (BlockAdded plus VirtualChainChanged { include_accepted_transaction_ids: false }), notification-envelope parsing for scaffold, wasm-style, and live upstream wrappers (blockAddedNotification / virtualChainChangedNotification with nested scope payloads), point RPC fallback over JSON wRPC for getBlock / getBlockDagInfo / getVirtualChainFromBlock when the endpoint URL is ws:// or wss://, a live capability probe surface that reads getServerInfo and getInfo, explicit rustls provider installation so wss:// works in-repo without ad hoc scripts, checked-in examples/live_wrpc_smoke.rs and examples/continuous_wrpc_smoke.rs, a new SubscriptionDriverEvent side-channel plus spawn_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_URL set, it connects to Postgres, runs migrations, ensures a subgraph schema, re-anchors IngestionState.prior_poi from the highest-DAA surviving POI before ingestion starts (so a restart resumes the same hash chain), prefers parsed notification-stream input when KASGRAPH_NOTIFICATION_JSONL is provided, can also consume websocket notification streams with KASGRAPH_NOTIFICATION_WS_URL plus optional KASGRAPH_NOTIFICATION_IDLE_TIMEOUT_MS, otherwise builds minimal live-style notifications (BlockAdded, VirtualChainChanged, RecoveryRequired), fetches one or more block hashes through kasgraph-rpc when KASGRAPH_RPC_PRIMARY_URL is configured, performs a live capability probe before continuous-mode subscription startup and now bails early on missing hasMessageId, missing hasNotifyCommand, or unsupported rpcApiVersion, 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 when VirtualChainChanged.removed_chain_block_hashes matches 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 to KASGRAPH_GAP_RECOVERY_BLOCK_HASHES only 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 pure validateManifest(value) (commit 5e6315f) checks a parsed subgraph.yaml against that documented contract — required fields, the closed kind/network enums, and the {kind:'typescript', file, entities[], handlers[]} mapping shape — returning one {path, message} issue per problem (empty array = valid). It is wired into kasgraph build right 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 in tests/sdk.test.ts). It deliberately does NOT validate kind-specific source contents or file existence (the caller's concern). A regression test (tests/cli.test.ts, commit 2760a84) asserts the kasgraph init scaffold's subgraph.yaml passes validateManifest, pinning the template to the same contract the build gate enforces so a fresh initbuild can'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), and KRC20_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) — Fingerprint with masked state windows, field-named extraction, and per-pattern registry covering 12 OpenSilver core patterns plus 5 KCC20 variants. Placeholder canonical bytes use a 0xFE-prefixed discriminator so no real chain script collides; real OpenSilver compiled bytes wire into the registry without touching the engine. A registry_schema() function + dump-registry binary 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, commit ebed8bf) — pure parse_krc20_inscription(payload) -> Krc20Parse for 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_ledger table are later slices.
  • Legacy KRC-20 ledger acceptance state machine (kasgraph-detectors::krc20_ledger, commit 83fd12f) — pure (no-Postgres) Krc20Ledger::apply(&inscription, sender) -> ApplyOutcome over a BTreeMap<tick, TokenState>. Kasplex rules: deploy first-writer-wins; mint requires amt <= lim AND minted + amt <= max (rejected wholesale, never partial); transfer/burn require sender_balance >= amt; burn decrements both balance and minted (saturating) so supply == sum(balances). Zero-balance entries pruned on debit. ApplyOutcome is Accepted | Rejected(reason). 11 unit tests incl. the supply invariant → detectors at 44. The kasgraph_krc20_legacy_ledger store 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, commit d629b02) — kasgraph_krc20_legacy_ledger (new migration 20260529120000), the durable journal of accepted Kasplex-era inscription ops behind the pure Krc20Ledger. 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); subgraph column scopes only reorg unwind; UNIQUE (tx_hash) is the replay idempotency key. Amounts are TEXT decimal strings (KRC-20 amounts are u64, can exceed i64::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, commit 7f30d30) — the NFT parallel to the legacy KRC-20 slice, both pure and storage-agnostic. parse_krc721_inscription(payload) -> Krc721Parse decodes 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 with tick_raw preserved; three-way NotKrc721/Malformed(reason)/Valid outcome). Krc721Ledger::apply(&inscription, sender) -> ApplyOutcome re-derives per-token ownership over a BTreeMap<tick, CollectionState> (max, owners: BTreeMap<id, owner>, minted: BTreeSet<id>): deploy first-writer-wins; mint requires id < max AND permanent uniqueness (a burned id stays in minted so it can never be re-minted); transfer/burn require current ownership. 21 unit tests → detectors at 65. The kasgraph_krc721_legacy_token / _transfer store 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, commit 4a35e11) — kasgraph_krc721_legacy_ledger (new migration 20260529130000), the durable journal behind the pure Krc721Ledger, NFT parallel of kasgraph_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); subgraph column scopes only reorg unwind; UNIQUE (tx_hash) is the replay idempotency key. token_id/max_supply are TEXT decimal strings, not BIGINT (KRC-721 ids/sizes are u64, can exceed i64::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/_transfer head 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, commit d593819) — Krc20Ledger::replay and Krc721Ledger::replay rebuild 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 model unwind_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, call replay) lands with the Postgres+address-gated wiring.
  • POI chain verifier (kasgraph-poi, commit f7c34d5) — 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 returns PoiVerification::Valid{final_poi} when every published POI matches, or Diverged{index, expected, recomputed} pinpointing the first block that doesn't (empty chain → Valid at the genesis prior). poi_from_hex decodes a published hex POI (inverse of poi_hex; rejects bad hex / wrong length via PoiError::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, commit 479f23e) — the read side that Krc20Ledger::replay / Krc721Ledger::replay consume, 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 as Vec<Krc*LegacyOpRecord> ordered by (accepting_daa_score, tick, seq) — a tick's intra-stream order is its monotonic seq, 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 shared LegacyOpRow type 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, commit a1e6617) — 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_record map a journaled op row's columns (op / amount|token_id / recipient / metadata_uri / max_supply / mint_limit) back into the Krc*Inscription that Krc*Ledger::replay consumes; replay_krc20_from_records / replay_krc721_from_records fold a fetched row slice (from fetch_krc*_legacy_ops_ordered) through replay — the row's sender is the already-resolved op sender, so the rebuild path needs no address resolution. Reconstruction returns Result<_, LegacyReconstructError> so a row a future bug stored malformed (unknown op, 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 beyond i64::MAX round-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_tests module, behind the same integration-pg feature) — verifies the node end of the wasm-dispatch loop that main.rs runs 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::load from a real on-disk build/manifest.json + wasm → seed store_get from Store::snapshot_entitiesdispatch_committed_hits → persist via Store::upsert_entity_version → reorg unwind_entity_versions. The fixture's WAT handleLock emits only on a store_get hit, 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. (In mapping_host.rs not a tests/ file because the node is bin-only.) Feature-gated off by default so cargo test --workspace stays 223; 63 node tests with the feature on. Run with DATABASE_URL=... cargo test -p kasgraph-node --features integration-pg.
  • KCC20 operation decoder (pure core), per-UTXO receipt model (kasgraph-detectors::kcc20_operation) — the covenant operation that was the last missing CovenantSpend field. Reconciled to the actual kcc20.sil reference contract after finding KasGraph's registry/reference assumed an aggregate total_supply/mint_nonce model the contract doesn't implement (per user decision: adopt the real model). KCC20 is per-UTXO: each covenant UTXO is a receipt with owner_identifier/identifier_type/amount/is_minter; a spend consumes an input receipt set and produces an output set. classify_kcc20_operation(prev, next) returns Mint (summed amount ↑), Burn (↓) — only a minter branch may change the sum per kcc20.sil checkAmounts — and with supply conserved, RotateController if the minter branch's controller binding (is_minter + COVENANT_ID owner_identifier) changed, else Transfer. as_str() emits the exact strings (transfer/mint/burn/rotate_controller) examples/krc20 branches on. Kcc20ReceiptState::from_payload parses the hex field map a KCC20Asset hit produces. The KCC20Asset registry fields were updated to the per-UTXO layout (owner_identifier/identifier_type/amount/is_minter) and cli/src/detector-schema.ts regenerated; KRC20_KRC721_REFERENCE.md corrected. 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 as krc20_ledger.
  • Phase 2.8 POI now hashes dispatched entity state (kasgraph-node) — POI moved out of the pure IngestionState transition into the persist loop (apply_and_persist_notification), computed after dispatch over each committed block's dispatched EntityVersionRecords (mapping_host::canonical_bytes_for_entitieskasgraph_poi::canonical_block_bytes) when a mapping is loaded, via the committed_poi_input selector; the no-mapping path keeps the block's detector-hit canonical bytes byte-for-byte. CommittedBlockWrite dropped its poi_hash field (the transition is now pure block-promotion), and prior_poi is 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, same integration-pg feature) — extends the store suite to the most intricate untested SQL: covenant_lineage_reorg_unwinds_and_reanchors_heads verifies the atomic 3-step unwind_covenant_lineage (delete rows ≥ cutoff → drop orphaned heads → re-point each surviving head at its highest-seq survivor via DISTINCT ON) — head re-pointing that no string test can observe; covenant_lineage_population_and_spend_lifecycle covers head/row population, the covenant_lineage_row_exists replay key, covenant_lineage_continues, and the spend record + DAA-scoped unwind; legacy_krc721_journal_seq_exists_fetch_unwind mirrors the KRC-20 journal round-trip for NFTs (incl. a u64 token id over i64::MAX). Writing these surfaced a real schema constraint — the lineage_row.covenant_id → lineage_head.covenant_id FK 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::test coverage (crates/kasgraph-store/tests/integration_pg.rs, behind the new integration-pg cargo feature) — every kasgraph-store query 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_schemaupsert_entity_versionlatest_entity/snapshot_entitiesunwind_entity_versions reorg drop → idempotent re-apply; (3) covenant-UTXO track/lookup/unwind; (4) the legacy-KRC-20 journal — seq allocation, replay guard, a record→fetch_krc20_legacy_ops_ordered round-trip proving the write/read column mappings agree (incl. a u64 overflowing i64), then DAA unwind; (5) POI + committed-block reorg re-anchor via unwind_committed_blocks_for_subgraph + latest_poi_for_subgraph. Feature-gated off by default so cargo test --workspace (and CI, no Postgres) stays green — default build/test/fmt/clippy verified clean in both feature states. Run with DATABASE_URL=... cargo test -p kasgraph-store --features integration-pg.
  • kasgraph-stream workspace test surface is green again: slow_subscriber_observes_lagged_error_then_resumes now drains retained backlog after the initial Lagged error before asserting fresh-event delivery, matching real tokio::broadcast semantics 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 Engine Config locks determinism: fuel metering (bounded execution; runaway handlers trip OutOfFuel instead of stalling the indexer), Cranelift NaN canonicalization, threads + relaxed-SIMD disabled. Each dispatch runs in a fresh Store for per-block isolation. ABI: guest exports memory + kasgraph_alloc(i32)->i32 + one handler(ptr,len) per manifest handler; guest imports kasgraph.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: OutOfFuelFuelExhausted, 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 (commit 3e32ca5). On covenant_id sources, CovenantSpent handler payloads now type as { spend: CovenantSpend; state: <stateType> }; CovenantLocked keeps the plain detector-state union (or unknown). CovenantSpend is 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) from subgraph.yaml, generates a thin AssemblyScript entry (build/entry.ts) that supplies kasgraph_alloc (bump allocator via the --runtime stub heap) and re-exports each manifest handler under the runtime's lookup name, then drives the AssemblyScript compiler (asc, a @kasgraph/cli dependency) with determinism flags (--optimize --runtime stub --use abort= — the last drops the stray env.abort import 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 outside kasgraph.{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 the kasgraph.store_get(ePtr,eLen,idPtr,idLen)->i64 host import so a handler can load a previously committed entity. Returns 0 on a miss; on a hit the host re-enters the guest's kasgraph_alloc, writes the entity data JSON 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-mapping AssemblyScript 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), and objStr / objU64 / objBool field accessors. JSON via assemblyscript-json. kasgraph build resolves it (and its transitive AS deps) by adding every ancestor node_modules as an asc --path root.
  • All six example mappings ported to AssemblyScript (commit ba94d0c). examples/{kasbonds,krc20,krc721,network-stats,opensilver-patterns,zk-proofs}/src/mapping.ts are 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 on spend.operation / successorCovenantId, supply + counter accumulation via store.get). tests/examples-build.test.ts runs kasgraph build on every example and asserts an ABI-valid wasm (exports memory + kasgraph_alloc + each handler; imports only kasgraph.{log,store_set,store_get}).
  • entity_versions persistence layer (kasgraph-store, commit 7acec60) — the substrate where wasm-mapping EntityOps land, versioned by DAA score so reorgs can unwind them alongside committed blocks. EntityVersionRecord / EntitySnapshotRow value types plus four Store methods: upsert_entity_version (idempotent on (entity_type, entity_id, block_daa_score)), latest_entity (highest-DAA row — seeds store_get), snapshot_entities (one row per key via DISTINCT ON ... ORDER BY block_daa_score DESC), unwind_entity_versions (deletes at or above a reorg cutoff). The per-subgraph entity_versions table is created by ensure_subgraph_schema; schema name is validated, so the format!-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 the kasgraph-node ingest loop.
  • Detector-hit → mapping → entity-version bridge (kasgraph-node::mapping_host, commit 9c8aa2e) — 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 typed MappingEvent from a lock-time detector hit; entity_versions(outcome, subgraph, daa) converts emitted EntityOps into DAA-stamped EntityVersionRecords; dispatch_locked_hit(...) runs a compiled MappingRuntime against a hit, seeding store_get from the committed EntitySnapshot, 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. Marked allow(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, commits 27a5a76 / 130efbe) — the bridge that lets the node learn handler resolution without parsing subgraph.yaml. The TS CLI stays the sole manifest parser: kasgraph build now also writes build/manifest.json ({name, wasm, dataSources:[{name, kind, patterns, collection, addresses, handlers:[{event, handler}]}]}) alongside the wasm. The node deserializes it with serde_json (no new YAML dependency) via subgraph_manifest::BuildDescriptor, which exposes load(dir), wasm_path(dir), and resolve_handler(detector_kind, event) — first data source whose patterns include 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, commits 429dfee / d52614d) — mapping_host::LoadedMapping::load(subgraph, dir) reads the descriptor, resolves + compiles the wasm into a MappingRuntime; 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 seeding store_get from the committed EntitySnapshot, and returns DAA-versioned EntityVersionRecords (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 via Store::snapshot_entities, persists each emitted record with upsert_entity_version, and the reorg path unwinds entity versions at the committed-unwind cutoff. The whole path is gated on KASGRAPH_SUBGRAPH_DIR: unset → exact prior node behavior (no dispatch). cargo test --workspace green (node at 46). Verified by unit tests + WAT fixtures; end-to-end against real Postgres still pending.
  • Spend-dispatch core (kasgraph-node::mapping_host, commit 412de54) — 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_SPENT manifest event (the locked covenant's detector kind resolves the spend handler); CovenantSpend envelope (operation / spentValueSompi / successorCovenantId, serde camelCase to match the CLI codegen's CovenantSpend interface — 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, seeding store_get from the committed snapshot and returning DAA-versioned records (mirrors dispatch_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, commit d9ec50c) — the first foundational slice toward wiring spend dispatch. IngestedTransactionInput (spending_tx_hash + previous_tx_hash + previous_output_index) + an inputs field on IngestedBlock, populated by a resilient extract_transaction_inputs walking each tx's inputs[].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_rpc sets inputs empty for now (BootstrapBlock doesn't carry inputs yet).
  • Covenant-UTXO tracker (kasgraph-store, commit 4e823d0) — the store-side lookup spend detection needs. Per-subgraph covenant_utxos table (PK (tx_hash, output_index); block_daa_score, detector_kind, covenant_id, locked_state JSONB) created by ensure_subgraph_schema. CovenantUtxoRecord / CovenantUtxoMatch value types + three Store methods: 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 validated SubgraphId schema name (injection-safe). 3 SQL-builder unit tests → kasgraph-store at 10.
  • Covenant-id + lineage assignment subsystem (kasgraph-detectors + kasgraph-node + kasgraph-store, commits ff9d26a / 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-None covenant_id is now real). Gated on a loaded mapping (no-mapping path unchanged, no extra lookups). (3) successorCovenantId is 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 (via Store::covenant_lineage_continues, an EXISTS check), else None (lineage terminates). The CovenantSpend envelope is now complete except operation. 143 workspace tests green. The genesis-vs-transition path needs Postgres to exercise end-to-end.
  • Detected covenant spends persisted (kasgraph-store + kasgraph-node, commit 50869a4) — a detected spend was only logged; now it lands in a per-subgraph covenant_spends table (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 / successorCovenantId stay absent until a spend-tx decoder can derive them. CovenantSpendRecord value 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-time covenant_utxos record — 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, commit 2dcf7b6) — the locked output's value is protocol-observable at lock time, so recording it lets a detected spend honestly report spentValueSompi without the spend-tx decoder. value_sompi BIGINT added to the covenant_utxos table, CovenantUtxoRecord, and CovenantUtxoMatch; 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 one CovenantSpend envelope field that needs no spend-tx decoding, so it lands ahead of operation / successorCovenantId. 137 workspace tests green.
  • Covenant-UTXO tracker wired into the node ingest loop (kasgraph-node, commit 6930ebd) — threads inputs through BootstrapBlock / block_from_rpc / block_to_rpc so previous-outpoint data reaches the node. On each CovenantLocked detector hit (gated on KASGRAPH_SUBGRAPH_DIR via mapping.is_some()), persists a CovenantUtxoRecord (locked_state = the hit payload) so the locked outpoint becomes spend-detectable. After applying a block, scans its inputs against the tracker via lookup_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 honest info! log. Actual CovenantSpent dispatch stays deferred: the spend envelope's operation / successorCovenantId / value can't be honestly populated without a spend-tx decoder, and the example mappings branch on spend.operation, so feeding a placeholder would corrupt them; the dispatch_spend_hit core (412de54) stays ready/dead_code. Reorg path unwinds covenant_utxos alongside entity versions. +1 threading assertion → node at 49 (137 workspace tests green). Same commit applies cargo fmt cleanup to previously-committed rpc/store/mapping_host code.

What's blocked on the user

  • 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.

What can be done autonomously next

  1. 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 / getInfo work there, generic subscribe acks come back as {"method":"subscribe","params":{"id":...}}, live notifications arrive as blockAddedNotification / virtualChainChangedNotification with nested scope payloads, point calls like getBlock, getBlockDagInfo, and getVirtualChainFromBlock also 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 with highest_daa_seen=444252802, reconnects=0, connections=1, and recovery_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 around spawn_continuous_subscription_with_events(...), then harden the anchor-based getVirtualChainFromBlock recovery path against those traces and keep pushing committed rollback semantics toward production-grade behavior per BLOCKDAG_REORG_SEMANTICS.md. Resolver-based discovery is still noisy from this environment (403 / 404 / 523 on other candidates), so generalized public-node discovery remains rough even though basic wire validation is no longer blocked.
  2. 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 via mapping_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 (CommittedBlockWrite dropped poi_hash); prior_poi is 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 real apply_and_persist_notification loop: a finalized block with an OpenSilverOwnable-fingerprint output → one committed hit → WAT mapping dispatches Bond/b1 → the persisted POI checkpoint is asserted equal to an independent compute_poi(genesis, canonical_bytes_for_entities(...)) and verify_poi_chainValid. 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_audit drain 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-block served_by row still covers subscription-delivered blocks.
  3. Phase 2.5 finisher — real fingerprints need an ANCHORED matcher, not byte-exact sync. Reconnaissance (2026-05-29) established silverc compiles kcc20.sil and emits script + 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's maxCovIns/maxCovOuts change 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 as kasgraph_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 real silverc compiles of kcc20.sil at bounds (4,4)+(8,8) via derive_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 stores int little-endian on chain, so Kcc20ReceiptState::from_payload now decodes amount LE. It is now wired into detection: DetectorEntry holds a PatternMatcher (Exact(Fingerprint) | Anchored(AnchoredFingerprint)); detect_in_output/registry_schema dispatch through it; KCC20Asset is Anchored(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: an xtask to generalize capture to the other loop-parameterized patterns (per-contract ctor args, programmatic const emission) + the 17-kind cross-collision check, then wire classify_kcc20_operation into the node spend path.
  4. Phase 2.8 — Extend the current committed-block write path into the real ingestion loop so every committed block emits a checkpoint.
  5. Per-detector payload codegen — DONE. @kasgraph/cli codegen now emits a <Kind>State interface per registered pattern: selector (covenant-state fields, all hex string, discriminated on detectorKind) and types each handler's payload as the union of its data source's detector states. Pattern-less sources (krc721 collection, utxo addresses) and unregistered selectors (the ZK-aware family) stay payload: unknown. The detector schema lives in cli/src/detector-schema.ts, regenerated from dump-registry via cli/scripts/gen-detector-schema.mjs.
  6. Phase 2.6 WASM mapping runtime — DONE. kasgraph-mapping is 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→WASM kasgraph build CLI command — compile an AssemblyScript mapping that targets this ABI (memory + kasgraph_alloc + handlers; imports kasgraph.log / kasgraph.store_set) — needs the asc toolchain 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.
  7. Spend-semantic payload codegen — DONE (commit 3e32ca5). CovenantSpent handler payloads on covenant_id sources now type as { spend: CovenantSpend; state }, with a once-emitted CovenantSpend interface 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".
  8. kasgraph build (TS→WASM compiler) — DONE. cli/src/build.ts compiles an AssemblyScript mapping to an ABI-compliant wasm via asc, generating the kasgraph_alloc + handler-re-export entry glue and verifying the output against the Phase 2.6 ABI (see "What's done"). Toolchain assemblyscript is now a @kasgraph/cli dependency.
  9. Port the examples/ mappings to AssemblyScript — DONE. Added entity reads (kasgraph.store_get) to the runtime ABI, shipped the @kasgraph/as-mapping authoring 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.ts compiles every example to an ABI-valid wasm. The runtime can now dispatch a real subgraph end-to-end.
  10. Wire dispatch into the ingest loop — DONE (commits 27a5a76 / 130efbe / 429dfee / d52614d). kasgraph build emits build/manifest.json; the node loads it + the compiled wasm (mapping_host::LoadedMapping) and dispatches committed detector hits through the runtime, seeding store_get from committed entity state and persisting emitted EntityOps as DAA-versioned rows, with reorg-time entity-version unwind. Gated on KASGRAPH_SUBGRAPH_DIR (unset → unchanged behavior). See "What's done".
  11. 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-pg feature — 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::load from disk → snapshot_entitiesdispatch_committed_hitsupsert_entity_version → reorg unwind_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 unblock CovenantSpent dispatch — a spend-transaction operation decoder. The CovenantSpend envelope is now honestly complete except operation: spentValueSompi is captured at lock time (2dcf7b6), covenant_id is assigned by lineage (4ea7fc6), and successorCovenantId is resolved from lineage continuation (edcb6b7). Only operation (the covenant's semantic action — transfer/mint/burn/rotate_controller) remains. CovenantSpent dispatch is now LIVE and verified end-to-end (node integration committed_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 the CovenantSpent handler, 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 actual kcc20.sil per-UTXO receipt model (each covenant UTXO = a receipt with owner_identifier/identifier_type/amount/is_minter): the operation is a pure function of the input/output receipt-set delta (summed amount ↑ → mint, ↓ → burn; supply held + minter-branch controller binding change → rotate_controller; else transfer), grounded in kcc20.sil's checkAmounts invariant, emitting the exact strings examples/krc20 branches 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's maxCovIns/maxCovOuts rewrite 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 honest info! log + reorg unwind (6930ebd), and now the operation classifier. Once real state extraction lands, classify the spent vs successor locked_state, fill operation, build the CovenantSpend envelope, and swap the info! log for a dispatch_spend_hit call; (c) then deploy/status/logs/remove CLI commands + Phase 5 hosted infra.

Performance targets to hit (from PLAN.md)

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%

Cross-project compounding

  • OpenSilver (sibling repo): KasGraph's kasgraph-detectors crate 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.