Repository navigation
feat(platform)!: let an indexOnly type index the elements of a typed array (PV14) - #5390
Conversation
…array (PV14) An indexOnly type may now name a typed array in an index, as a stored type can since #5370: a document has one entry per distinct element of its list, and none for an empty, null or absent list. Registration (apply_index_only, every parse) admits such an index when another index keeping one entry per document (no $createdAt, skip, outlivesDelete or typed array) has its whole key inside the array index's key without the array, the array declares uniqueItems, and the index is neither preallocated nor a summableOffCountIndex index. An index naming a typed array is never the proof index or a counter's source. The row commitment binds the whole list in its order, so a delete carrying the tags reordered, shortened or changed is refused; an empty, null and absent list are committed alike. The entry probes fan out per element, and synthesis rebuilds an entry as a document listing its one element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to The change lets index-only document types, such as Yappr likes, index each tag in a list. It restricts which indexes may do this, and the review found no open defect in fee charging or required-value storage. Deletes must supply the tags in their original order, which is the stated design. The change is ready to merge after normal checks. Pre-merge checks |
|
|
📖 Book Preview built successfully. Download the preview from the workflow artifacts. Updated at 2026-10-12T19:18:34.227Z |
|
🔍 Review in progress — actively reviewing now (commit 82a4d3b) · triage: critical |
|
Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
- the entry probes judge the skip set with the walkers' own definition (`takes_part_in_index_by` / `document_carries`) - the create's state probe skips an index of a typed array: the covering index registration requires is probed and refuses any collision first - apply_index_only also refuses, on every parse, an indexOnly index naming two typed arrays or a nested one - the covering-key check is shared with the outlivesDelete rule - the row commitment iterates the properties once - the ContainsAny doc notes an indexOnly read returns a document per value - tests: the proof index when a tag index sorts first, a delete without $createdAt when only an outliving window involves it, and a contract update dropping uniqueItems Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 2 threads unresolved — resolve them. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Verified the supplied findings against exact head 4b56ddc. Two blocking issues remain: implicit array sparsity can leave required scalar values unrecoverable, and the newly admitted array commitment work is not charged on insertion; the PV14 snapshot also contains a stale rejection rule. This was static verification only: no builds or tests were run, and Rust workspace CI was queued in the supplied 2026-10-12T18:15:10Z snapshot.
🔴 2 blocking | 🟡 1 suggestion(s)
Review provenance
Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — This is a large, intricate change to consensus-critical document indexing, validation, deletion, querying, storage keys, and fee accounting in the protocol-versioned Drive/DPP state machine, including the functions that add and remove index entries and synthesize index-only results. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(antigravity below 15% reserve: weekly 14% left, 5h 100% left),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs`:
- [BLOCKING] packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs:4643-4648: Account for implicit array skips in required-property coverage
The new admission exposes a required-value storage hole. `parse_indices` adds an indexed array to `skip_if_absent_properties` while leaving the declaration boolean `skip_if_absent` false, but required-property coverage below still accepts an index whenever `!index.skip_if_absent && !index.outlives_delete`. A type with required `postId` and `postAuthor`, optional `tags`, `byPost = [postId] -> $ownerId`, and `byTagPostAuthor = [tags, postId, postAuthor] -> $ownerId` therefore passes both the covering-key rule and property coverage. With absent or empty tags, the array index writes nothing and only `byPost` remains: `postAuthor` is hashed into the row commitment but stored in no recoverable entry. A client that loses that value cannot reconstruct the tuple required for deletion. For required scalar coverage, check effective participation rather than the declaration boolean, while retaining the array's empty-list semantics. Add regression coverage for validating and non-validating parses of this shape.
- [SUGGESTION] packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/common/mod.rs:4643-4648: Record the indexOnly typed-array rule in the PV14 version snapshot
This branch now admits qualifying indexOnly typed-array indexes, but item 92 in `packages/rs-platform-version/src/version/v14.rs` still states that parser generation 3 refuses indexes on an indexOnly type. Amend that item and the `PLATFORM_V14` contract-version annotation to describe the new admission constraints and their PV14 activation boundary. No additional version slot is needed for this finding: the affected indexOnly and typed-array data is unavailable in historical protocol generations, and PV14 is unreleased.
In `packages/rs-drive/src/drive/document/index_only_row_commitment.rs`:
- [BLOCKING] packages/rs-drive/src/drive/document/index_only_row_commitment.rs:230-236: Typed-array row commitment work is not charged on create
The newly admitted array path serializes every element into the commitment preimage, and the terminal insert builder recomputes and double-hashes that full preimage for every emitted entry, including each array element and time window. In `add_reference_for_index_level_for_contract_operations/v0/mod.rs`, the commitment call at lines 549–553 adds no corresponding `FunctionOperation`; estimation instead constructs a padded placeholder value. GroveDB operations account for storing the fixed-size commitment and maintaining the trees, not this separate hash over the full list. The delete validation path explicitly charges its commitment with `FunctionOp::new_with_byte_count(HashFunction::Sha256_2, preimage_size)`. Although missing scalar-insert hash billing predates this PR, this change newly increases the uncharged work through larger preimages and array fan-out, so the incremental defect is in scope. Account for it in Drive's insertion operation assembly and its estimate, or compute and charge one shared commitment per document. An estimate-versus-applied-fee comparison alone cannot detect work omitted from both fee totals.
…lues out of array-only coverage - an index of a typed array no longer covers a required property other than the array itself: it has no entry for a document whose list is empty, so such a property could be committed and stored nowhere - every indexOnly entry insert bills its row commitment's double-SHA256 by its preimage; the dry run bills the widest preimage the type allows - the PV14 notes (item 92 and the contract versions annotation) describe the indexOnly typed array rules Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai ✓ · thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
…-where #5388 and #5390 are merged (squashed, with their review fixes). Resolved as v5.0-dev plus this branch's own changes: the export list keeps where_list_elements beside where_lists_elements_read, note 94 keeps the element billing and points at 95, note 92 now says preallocated and counter array indexes follow 95, and the book keeps both the commitment billing and the per-element keying.
Basic explanation
What this does: An index-only document type (one whose documents exist only as index entries, like Yappr's
like) can now have an index over the items of a list property, such as a post'stags. A like taggeddashandnewsgets one entry underdashand one undernews, so apps can count and list likes per tag. Until now the contract parser refused this shape.Value: Yappr is moving a post's single
hashtagto atagslist (up to 4). With this, itsliketype keeps per-tag counts and per-tag trending working, at one entry per tag instead of a separate document per tag.Risks: Medium. It touches how index-only documents are created, checked on delete and read back, all on protocol version 14, which is unreleased. Every contract valid today parses and behaves the same: no index-only type could name a list before. The main new rule is that another index must keep each document's entries apart, so the list index never becomes an accidental uniqueness rule, and deletes must repeat the exact list (same items, same order) the like was created with. Fee estimates were checked to stay at or above the real fee for 1 and 4 tags, with and without a time window. Every index-only create now also pays a small processing fee for hashing its row commitment once per entry, which was previously unbilled work.
Issue being fixed or feature implemented
Since #5370 (and the SDK side in #5372) a stored document type can index each element of a typed array. An indexOnly type was refused (
validate_indexed_array_index: "an indexOnly type's indexes are uniqueness rules over each document's values, and an index of a typed array enters a document once per element"). Yappr'slikeis indexOnly and needs a row per tag once posts carrytags(typed array,maxItems4,uniqueItems, immutable), so per-tag counts and trending keep working.This PR covers the indexOnly per-element index only. Two related pieces are separate work and stay as they are:
refersTowhereentries over typed arrays;preallocated/summableOffCountIndexthrough an array-equalitywhere. Both stay refused on an index over a typed array here.Before
After
The same contract registers. A like
{ postId: P, postAuthor: A, tags: ["dash", "news"] }by owner O writes:each entry holding the same row commitment. A like without tags (absent,
nullor[]) is in neither tag index.Refused shapes, each with
InvalidContractStructure(10231):{ "name": "byTagPost", "properties": [{ "tags": "asc" }, { "postId": "asc" }] }with no index whose whole key is inside
{postId, $ownerId}(for example only[postAuthor] -> $ownerIdbeside it): "its key without the array ... holds the whole key of no index that keeps one entry for every document"; with"uniqueItems": falseontags: "must declareuniqueItems: true"; with"preallocated": true, or as asummableOffCountIndexindex: refused naming that keyword.What was done?
Registration (
rs-dpp,apply_index_only, which runs on every parse, validating or not). An index of an indexOnly type that names a typed array (Index::indexed_array) is admitted when:keys_each_live_document_by_its_values: keeps entries, skips no document, does not outlive deletes, involves no$createdAt, and now also names no typed array) and its whole key (properties and terminal components) is inside this index's key without the array. Two live documents could then only share an entry of the array index by sharing that index's entry, which the second create is refused for: the array index is no uniqueness rule of its own, and no entry of it stands for two documents;uniqueItems: true, so each element of a list has an entry of its own (and an update can not remove it, since every parse runs the rule);preallocatednor asummableOffCountIndexindex;The covering check shares one helper (
keyed_by_a_document_index) with the existing outlivesDelete rule, which asks the same question.Required-property coverage no longer counts an index of a typed array for any property but the array itself: it has no entry for a document whose list is empty, so a required
postAuthorheld only by[tags, postId, postAuthor]would be committed and stored nowhere for an untagged like. Such a type is now refused.The #5370 rules (one array, top-level, not unique or contested,
nullSearchable, no ranking above the array,maxItemsat most 16, or 4 under a window, 256 entries per document) still apply. The old indexOnly refusal invalidate_indexed_array_indexis gone.keys_each_live_document_by_its_valuesexcluding typed arrays also means an array index is never the proof index (it has no entry for an empty list) and never a counter's source.Row commitment (
index_only_row_commitment). A typed array is committed as its whole list in the document's order:name ‖ u32 len ‖ (u32 len ‖ element key)*, each element keyed as its entries are (serialize_value_for_key). A delete carrying the list reordered, shortened, lengthened or changed hashes differently and fails every probe,byPost's included. An empty,nulland absent list write the same entries and are committed alike (nothing, name included), so a delete may carry any of the three.Entry paths (
index_only_entry_paths_and_key). The array's level fans out over the distinct sorted elements (get_raw_array_elements_for_document_type, what the write walkers use), times the windows of a bucketed first level; a list without elements gives no paths. The skip set is now judged with the walkers' own definition (takes_part_in_index_by/document_carries). The delete commitment probe, its fee estimate, the batch entry tracker and the client document cost all read these paths, so they see every element. The write and delete walkers already fanned out per element (#5370) and needed no change.Commitment hash billing (
add_reference_for_index_level_for_contract_operations/v0). Every indexOnly entry insert now bills its row commitment's double-SHA256 as aSha256_2function operation sized by the real preimage; the dry run bills the widest preimage the type allows (index_only_row_commitment_max_preimage_size), so the estimate stays an upper bound. A typed array makes both the preimage and the number of entries larger, so this is now paid for. This applies to every indexOnly create, with or without a typed array.Create probe (
document_create_transition_action/state_v1). The create's state validation no longer reads an array index's entries: the covering index is probed and refuses any collision first, so the reads per element could never find a duplicate. The storage insert's if-not-exists stays the backstop.Queries (
synthesize_index_only_document). An entry under an element is rebuilt as a document listing that one element (tags: ["dash"]), a list of the array's type rather than a bare element.containsandcontainsAnyroute through the existing matchers;containsAnyreturns a like once per listed tag it holds, each a distinct projection.Comments and book. Comments that assumed an indexOnly type indexes no typed array now give the actual reason.
book/src/contract-keywords/index-only.mdgets a "Typed arrays" section with the example and rules;typed-arrays.md,indexes.mdandbook/src/drive/index-only-document-types.md(commitment encoding, constraint matrix) follow.In-place changes to shipped generations
No
vNwas added: protocol version 14 is unreleased, and every edited path is reached only by data that exists from it.try_from_schema/common/mod.rs(shared by document type generations 1 to 3, protocol versions 9 to 14): the removed refusal sits invalidate_indexed_array_index, which runs only whenadmit_indexed_arraysis set (generation 3, protocol version 14).index_only_array_index_erroris called only fromapply_index_only, which only generation 3 calls. Thesummable_off_count_index_errorwording change is in the same generation 3 path.Index::keys_each_live_document_by_its_values(unversioned): read only byapply_index_only,summable_off_count_index_errorand Drive'sindex_only_proof_index, all indexOnly-only, and indexOnly types exist only from protocol version 14. The new term readsindexed_array, which isNoneon every index of every earlier generation.index_only_entry_paths_and_key,index_only_row_commitmentandsynthesize_index_only_document(unversioned): reached only for indexOnly types (protocol version 14). The new branches fire only for an index or property that names a typed array, which no indexOnly type could do before this PR. The skip-set check moving to the walkers'document_carriesdiffers from the oldget_raw_for_document_type(..).is_none()only for a skip value present asnull, which the schema refuses on every indexOnly transition.add_reference_for_index_level_for_contract_operations/v0(the terminal writer): the hash billing sits inadd_index_only_terminal_item_operations, reached only for indexOnly types (protocol version 14).document_create_transition_action/state_v1(document create state validation): the probe filter skips indexes withindexed_array, which only an indexOnly type of protocol version 14 can carry after this PR; the branch is insideif document_type.index_only().How Has This Been Tested?
New tests:
rs-dppindexed_array_tests(validating and non-validating parses): admits the Yappr shape, with the array in each tag index's skip set and no tag index keeping each document once; refuses an array index no other index keeps apart (an index outside its key, and candidates involving$createdAt); refuses one withoutuniqueItems; refusespreallocatedandsummableOffCountIndexon an array index; refuses an indexOnly index naming two arrays or a nested one on every parse; refuses a required property only an array index holds, while a required array its own index holds parses.rs-driveindex_only_array_index_e2e_tests(13 tests): every entry's commitment hash is billed, oneSha256_2per entry (22 for a 4-tag like), by the real preimage when applied and the type's widest in the dry run; an array index sorting first is never the proof index, and a tagged and an untagged like are each proven throughbyPost; when only the outliving window involves$createdAt, a like deletes by its values without a timestamp, leaves its per-tag window entries, refuses reordered tags and drains later; one entry per tag underbyTagPostand per window and tag underbyTrendTagPost, all carrying the row commitment, and the probes read exactly those paths; a second like of a post by its owner is refused whatever its tags while another owner's like with the same tags goes through; a delete removes every per-tag entry and prunes a tag no other like holds; deletes with tags reordered, shortened, lengthened, changed, empty,nullor absent are refused and remove nothing; absent,nulland empty tags enter no tag index and delete in any of the three forms; per-tag window entries outlive a delete and drain with their window, and a like whose cleared windows drained still deletes; dry-run fees upper-bound applied fees (insert storage and processing, delete processing) for 1 and 4 tags with no window, an outliving window and a cleared window;contains,contains+postId, the terminal-bound "did I like this under this tag" read andcontainsAnyprove to the live root hash; per-tag counts, a post's count under a tag,containsAnygrouped counts and a window's count under a tag agree unproved and proved (point lookup verifier).rs-drivefixtureyappr-likes-tags-contract.jsonadded tofixture_contracts::CONTRACTS, so the layout and client cost checks (document::cost,document::layout) run over it against real inserts.rs-drive-abcicontract_structure_error_tests: a contract update droppinguniqueItemsfrom the tags a stored indexOnly index names is refused, paid, incheck_txand the block.rs-drive-abciindex_only::array_index_tests: throughprocess_raw_state_transitions, a tagged like is created and proven through the proof index, a second like of the post under another tag is refused withDuplicateUniqueIndexError, a delete with the tags reordered is refused withDocumentNotFoundError, and the tags as created delete and are proven absent.Targeted runs, all passing:
Breaking Changes
Consensus: at protocol version 14 (unreleased) a contract with an indexOnly index over a typed array that meets the rules above now registers, where it was refused, and every indexOnly create's processing fee now includes its commitment hashes. No earlier protocol version changes.
Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
82a4d3b/skip-botsproceeds without the ones not yet reported/self-reviewedWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.Summary by CodeRabbit