Skip to content

fix(drive): store the current-key alias for bound encryption/decryption keys under the purpose subtree - #4832

Closed
PastaPastaPasta wants to merge 2 commits into
dashpay:v4.2-devfrom
PastaPastaPasta:fix/contract-bound-enc-key-current-alias-path
Closed

PastaPastaPasta wants to merge 2 commits into
dashpay:v4.2-devfrom
PastaPastaPasta:fix/contract-bound-enc-key-current-alias-path

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Sep 18, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

An IdentityUpdate that adds an ENCRYPTION or DECRYPTION key with ContractBounds::SingleContract fails inside Drive when the bound contract's config sets requiresIdentityEncryptionBoundedKey / requiresIdentityDecryptionBoundedKey to 2 (StorageKeyRequirements::MultipleReferenceToLatest). Validation passes, the apply hits a grovedb MissingReference, and the transition ends as an InternalError: no nonce bump, no fee, the identity unchanged, and the client's wait times out with no consensus error to show.

InternalError("storage: grovedb: missing reference reference to
path:`20/<identity>/20/<contract>/01` key:`02` in batch is missing")

2 is the value the DashPay system contract uses and the value every contract published from the JS/wasm SDK gets by default, so this is the common case. The Unique (0) and Multiple (1) modes work, and document-type-level bounds (SingleContractDocumentType) work in every mode.

Not a protocol-14 regression: the same drop reproduces on testnet today (drive 4.1.0, protocol 13, apply v0), and the lines at fault are unchanged since the feature landed in #1358 (2023). Contract-level bound ENCRYPTION/DECRYPTION keys under opt-in 2 have never worked on any network. Yappr hit this in March 2026 and worked around it by registering unbound keys.

Cause. add_potential_contract_info_for_contract_bounded_key inserts the per-key reference under the purpose subtree, then for MultipleReferenceToLatest adds a "current key" alias as a SiblingReference(key_id) at the empty key. For non-AUTHENTICATION purposes the alias was written one level up, in the keys subtree (identity_contract_info_group_keys_path_vec) instead of the purpose subtree (identity_contract_info_group_path_key_purpose_vec). A sibling reference resolves within its own path, so the alias pointed at .../keys/<key_id>, which does not exist, and grovedb refused the batch. The AUTHENTICATION branch (#4780) and the document-type branch already used the purpose subtree, and the query side (IdentityKeysRequest::into_path_query, ContractBoundKey(_, _, CurrentKeyOfKindRequest)) has always read the alias from the purpose subtree's empty key. refresh_potential_contract_info_key_references mirrored the same wrong path.

What was done?

  • add_potential_contract_info_for_contract_bounded_key/v1: the current-key alias goes under the purpose subtree for every purpose, matching the document-type branch and the query.
  • refresh_potential_contract_info_key_references/v1: same path for the refresh written when such a key is disabled.
  • identity/key/fetch/mod.rs: AllKeysOfKindRequest skips the empty-key alias (RangeAfter([]..)) for every purpose, not only AUTHENTICATION, so listing bound ENCRYPTION/DECRYPTION keys no longer returns the newest key twice. Docs updated.
  • Comments in contract_info/keys/mod.rs, the low-level batch apply, drive_identity_method_versions/v2.rs and v14.rs updated to describe the real layout.

Versioning. v1 of both methods is fixed in place; no v2, no method-table bump. v1 is selected only by protocol 14, and the old v1 path never produced committed state on any network: the batch always failed, prepare_proposal removed the transition, and process_proposal rejects blocks carrying an InternalError. Replay under old and new v1 is therefore identical, and no migration is needed. v0 (protocols ≤ 13) is untouched. If maintainers prefer a v2 plus a table bump for protocol 14, that is a mechanical change to this PR.

Audit of other alias readers/writers. Estimation helpers already account for the alias at the purpose level (the keys level now matches its AllSubtrees estimate). The proof verifier and verify_identities_contract_keys reuse identities_contract_keys_query, which reads the purpose subtree's empty key; no change. AllKeysOfKindRequest on contract-bound requests is not reachable from consensus or gRPC (tests only), so the listing change has no on-chain effect.

Not changed, noted for reviewers. Two same-purpose mode-2 keys bound to the same contract in one IdentityUpdate would both try to insert the alias (pre-existing; the coalescer only handles AUTHENTICATION). identities_contract_document_type_keys_query uses contract_id instead of group_id in its subquery path (pre-existing).

How Has This Been Tested?

Regression test test_identity_update_adding_contract_bound_key_multiple_reference_to_latest (drive-abci): the existing contract-bound key test with the contract's opt-in flipped from 0 to 2. Confirmed failing before the fix with the MissingReference error above. After the fix it registers an ENCRYPTION and a DECRYPTION key in one update with grovedb consistency verification on, asserts the current-key and all-keys queries per purpose, then disables the ENCRYPTION key to exercise the refresh path and re-verifies the tree.

New drive test test_fetch_contract_bound_encryption_and_decryption_keys_with_multiple_reference_to_latest: current key, all keys, fetch_identities_contract_keys, fee estimate ≥ actual, grovedb verify.

cargo test -p drive-abci --lib -- identity_update                              30 passed
cargo test -p drive-abci --lib -- identities_contract_keys contract_bounds identity_create   268 passed
cargo test -p drive -- contract_info key::fetch                                102 passed
cargo clippy -p drive -p drive-abci --all-targets -- -D warnings               clean
cargo fmt --all -- --check                                                     clean

On-network reproduction before the fix: devnet moutai (4.2.0-beta.2, protocol 14) and testnet (drive 4.1.0, protocol 13) both drop a SingleContract-bound ENCRYPTION key against a contract opting in with 2 (wait times out, nonce and balance unchanged, key absent), while opt-in 0/1, an unbound control key, and a SingleContractDocumentType bound key against DashPay contactRequest are all accepted.

Not run: the full workspace suite, strategy tests, a post-fix retest on a live network.

Breaking Changes

None. Nothing ever landed at the old alias path on any network, so state layouts of existing chains are unaffected; only transitions that previously failed now succeed.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PastaPastaPasta and others added 2 commits September 18, 2026 15:48
…ultipleReferenceToLatest

Registers a MEDIUM ENCRYPTION key with SingleContract bounds against a contract whose config opts in with requiresIdentityEncryptionBoundedKey = 2. The existing test only covers the Unique (0) mode. This one fails on v4.2-dev: Drive inserts the current-key sibling reference beside the purpose subtree instead of inside it, grovedb reports a missing reference, and the transition ends as an InternalError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on keys under the purpose subtree

An IdentityUpdate adding an ENCRYPTION or DECRYPTION key bound to a contract whose config opts in with requiresIdentityEncryptionBoundedKey / requiresIdentityDecryptionBoundedKey = 2 (MultipleReferenceToLatest) failed inside Drive with a grovedb MissingReference and was dropped as an InternalError. The current-key alias is a sibling reference at the empty key, but for non-AUTHENTICATION purposes v1 wrote it one level above the purpose subtree that holds the key id it names, so it could not resolve. The AUTHENTICATION and document-type branches, and every reader (CurrentKeyOfKindRequest, identities_contract_keys_query), already used the purpose subtree.

Both v1 methods now use the purpose subtree for every purpose. AllKeysOfKindRequest skips the empty key for every purpose so listings never repeat the alias. v1 is fixed in place: the old path never produced a committed block on any network (the batch always failed and the transition was removed from the proposal), so replay is unaffected and no migration is needed; v0 is untouched. Comments and the v14 changelog describe the real layout. The regression test now covers both purposes, both key-kind queries, and the refresh path on disable; a drive-level test covers the fetch and getIdentitiesContractKeys reads, the fee estimate, and grovedb consistency.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: dashpay/platform/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e930d8c2-755a-443b-85b8-7090d360c54c

📥 Commits

Reviewing files that changed from the base of the PR and between 51183be and 6999335.

📒 Files selected for processing (8)
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/identity_update/mod.rs
  • packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs
  • packages/rs-drive/src/drive/identity/contract_info/keys/mod.rs
  • packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs
  • packages/rs-drive/src/drive/identity/key/fetch/mod.rs
  • packages/rs-drive/src/util/operations/apply_batch_low_level_drive_operations/v1/mod.rs
  • packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs
  • packages/rs-platform-version/src/version/v14.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

PR Hygiene

State: waiting-bots · commit 6999335ba1365b2361dbfcb8a870edafb6924f3b

  • Bot changes request remains outstanding
  • Bot review threads remain unresolved
  • Proceeded without coderabbitai: no review within the configured window
  • Proceeded without thepastaclaw: no review within the configured window

Self-review is an author attestation that you have read the diff:
/self-reviewed — covers everything pushed so far; post it again after a new push.

This check passes when the policy is satisfied; the repository decides whether merging requires it.

@thepastaclaw

thepastaclaw commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 2 blocking finding(s) (commit 6999335) · triage: normal · Phase 2 only (queue backlog)

@github-actions github-actions Bot added this to the v4.2.0 milestone Sep 18, 2026

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Phase 2 only (queue backlog)

The alias relocation matches the existing current-key readers, but newly enabled atomic key rotations can retain the disabled key as current, and all-keys queries now omit Unique-mode keys. The writer changes also alter shipped protocol-14 behavior without a new activation boundary. Independent source inspection confirms these three issues; git diff --check passed, but the Rust tests were not independently rerun.

🔴 2 blocking | 🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 2: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); reviewer 4: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); final verifier: gpt-6-astra (agent: astra-verifier, role: final-verifier)

  • Triage: normal by gpt-6-astra (effort low) — The behavioral changes are modest alias-path and query-range fixes with substantial regression tests, but their interaction with key disabling, storage modes, and protocol-version compatibility warrants ordinary review rather than qualifying as a large or intricate critical-surface change.
  • Phase 1 reviewers: not run (skipped for throughput: 21 PRs queued, above the 10 limit)
  • Fresh verifier: gpt-6-astra — final-verifier; agent astra-verifier
  • Phase 2 reviewers: gpt-6-astra — general (completed, effort high); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6-astra — platform-versioning (completed, effort high); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort high); agent phase2-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-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs`:
- [BLOCKING] packages/rs-drive/src/drive/identity/contract_info/keys/refresh_potential_contract_info_key_references/v1/mod.rs:212-216: Coalesce alias insertion and refresh during bound-key rotation
  An IdentityUpdate that registers a replacement bound ENCRYPTION or DECRYPTION key and disables the previous current key queues registration before revocation. With the corrected paths, registration inserts the replacement alias and revocation then queues an untrusted refresh for exactly the same slot. The coalescer still recognizes only AUTHENTICATION, so GroveDB's batch map keeps the later refresh instead of the insertion. That refresh reads the existing on-disk reference, leaving the disabled key current; enabling batching consistency verification instead rejects the duplicate operations. Although document-type bounds already had this problem, this PR newly exposes it for contract-level MultipleReferenceToLatest keys. Extend alias coalescing through a versioned batch-application path so insertion wins over refresh for both newly enabled purposes, and add same-update registration-plus-revocation tests with consistency verification both enabled and disabled.

In `packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs`:
- [BLOCKING] packages/rs-drive/src/drive/identity/contract_info/keys/add_potential_contract_info_for_contract_bounded_key/v1/mod.rs:307-312: Activate the storage repair through a new protocol version
  Both alias writers change their existing v1 implementations while the protocol-14 method table still selects generation 1. CHANGELOG.md explicitly records protocol 14 as shipped in 4.2.0-beta.2, and the versioning book requires shipped behavior to remain frozen. The fact that the broken path never committed state establishes that a migration may be unnecessary, but not that old and patched nodes agree: a patched proposer can successfully include this IdentityUpdate, whereas an unpatched validator encounters MissingReference and process_proposal rejects proposals containing failed transitions. Preserve the existing v1 insertion and refresh implementations, introduce new generations, and activate them only at the next unreleased protocol boundary. Changing protocol 14's selectors alone would not resolve that disagreement.

In `packages/rs-drive/src/drive/identity/key/fetch/mod.rs`:
- [SUGGESTION] packages/rs-drive/src/drive/identity/key/fetch/mod.rs:958-961: Preserve Unique-mode keys in all-keys queries
  The empty-key entry is not always a duplicate alias. For Unique-bound ENCRYPTION and DECRYPTION keys, both the v0 and v1 insertion implementations store the only reference at vec![], without a key-ID entry. Replacing RangeFull with RangeAfter therefore changes AllKeysOfKindRequest from returning that key to returning no keys. The document-type branch below has the same regression, and into_path_query has no PlatformVersion parameter, so selecting protocol 13 does not preserve the previous behavior. These listing requests are not currently consensus or gRPC callers, which limits the severity, but the public Drive fetch API still loses supported results. Preserve Unique-mode results while removing MultipleReferenceToLatest duplicates, retain historical query behavior through version selection wherever semantics differ, and cover both storage modes and both bounds types. Redirecting callers to CurrentKeyOfKindRequest in documentation does not preserve the existing API behavior.

Comment on lines +212 to +216
let sibling_path = identity_contract_info_group_path_key_purpose_vec(
&identity_id,
&root_id,
purpose,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Coalesce alias insertion and refresh during bound-key rotation

An IdentityUpdate that registers a replacement bound ENCRYPTION or DECRYPTION key and disables the previous current key queues registration before revocation. With the corrected paths, registration inserts the replacement alias and revocation then queues an untrusted refresh for exactly the same slot. The coalescer still recognizes only AUTHENTICATION, so GroveDB's batch map keeps the later refresh instead of the insertion. That refresh reads the existing on-disk reference, leaving the disabled key current; enabling batching consistency verification instead rejects the duplicate operations. Although document-type bounds already had this problem, this PR newly exposes it for contract-level MultipleReferenceToLatest keys. Extend alias coalescing through a versioned batch-application path so insertion wins over refresh for both newly enabled purposes, and add same-update registration-plus-revocation tests with consistency verification both enabled and disabled.

source: gpt-6-astra (phase2-reviewer: general)

Comment on lines 307 to +312
let sibling_ref_type_path = SiblingReference(key_id_bytes);
let sibling_path = if purpose == Purpose::AUTHENTICATION {
// A bound authentication key's current-key reference belongs beside
// its key IDs, under the purpose subtree. Keep legacy paths frozen.
identity_contract_info_group_path_key_purpose_vec(
&identity_id,
&root_id,
purpose,
)
} else {
identity_contract_info_group_keys_path_vec(&identity_id, &root_id)
};
let sibling_path = identity_contract_info_group_path_key_purpose_vec(
&identity_id,
&root_id,
purpose,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Activate the storage repair through a new protocol version

Both alias writers change their existing v1 implementations while the protocol-14 method table still selects generation 1. CHANGELOG.md explicitly records protocol 14 as shipped in 4.2.0-beta.2, and the versioning book requires shipped behavior to remain frozen. The fact that the broken path never committed state establishes that a migration may be unnecessary, but not that old and patched nodes agree: a patched proposer can successfully include this IdentityUpdate, whereas an unpatched validator encounters MissingReference and process_proposal rejects proposals containing failed transitions. Preserve the existing v1 insertion and refresh implementations, introduce new generations, and activate them only at the next unreleased protocol boundary. Changing protocol 14's selectors alone would not resolve that disagreement.

source: gpt-6-astra (phase2-reviewer: architecture-layering)

Comment on lines 958 to +961
AllKeysOfKindRequest => {
if purpose == Purpose::AUTHENTICATION {
// Bound authentication keys keep their current-key alias at the
// empty key of the purpose subtree; listing must not repeat it.
Query::new_single_query_item(QueryItem::RangeAfter(vec![]..))
} else {
Query::new_single_query_item(QueryItem::RangeFull(RangeFull))
}
// Every purpose keeps its current-key alias at the empty key of the
// purpose subtree; listing must not repeat it.
Query::new_single_query_item(QueryItem::RangeAfter(vec![]..))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Preserve Unique-mode keys in all-keys queries

The empty-key entry is not always a duplicate alias. For Unique-bound ENCRYPTION and DECRYPTION keys, both the v0 and v1 insertion implementations store the only reference at vec![], without a key-ID entry. Replacing RangeFull with RangeAfter therefore changes AllKeysOfKindRequest from returning that key to returning no keys. The document-type branch below has the same regression, and into_path_query has no PlatformVersion parameter, so selecting protocol 13 does not preserve the previous behavior. These listing requests are not currently consensus or gRPC callers, which limits the severity, but the public Drive fetch API still loses supported results. Preserve Unique-mode results while removing MultipleReferenceToLatest duplicates, retain historical query behavior through version selection wherever semantics differ, and cover both storage modes and both bounds types. Redirecting callers to CurrentKeyOfKindRequest in documentation does not preserve the existing API behavior.

source: gpt-6-astra (phase2-reviewer: general)

@github-actions github-actions Bot added the bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. label Sep 19, 2026
@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Superseded by #4843: same commits and head (6999335ba1), reopened from a dashpay/platform branch so the Rust workspace CI runs (fork pull requests skip it).


🤖 Posted autonomously by Claude on behalf of pasta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-review-skipped A required review bot did not report; it was skipped by the window or by a person.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants