Skip to content

chore: merge v5.0-dev into v5.1-dev - #5481

Open
shumkov wants to merge 8 commits into
v5.1-devfrom
chore/merge-v5.0-rc4-into-v5.1
Open

shumkov wants to merge 8 commits into
v5.1-devfrom
chore/merge-v5.0-rc4-into-v5.1

Conversation

@shumkov

@shumkov shumkov commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Basic explanation

What this does: Brings v5.1-dev up to date with v5.0-dev at f282f29999: the 5.0.0-rc.4 release (six PRs) and #5026, which landed on v5.0-dev after rc.4. 5.0.0-rc.3 and #5431 already reached v5.1-dev through #5432, whose merge base (51ee8a7261) comes after rc.3.

Value: v5.1-dev gets the rc.4 fixes:

It also gets #5026's persisted DashPay coreHeight backfill coverage.

Risks: Low to moderate.

Issue being fixed or feature implemented

v5.1-dev was seven PRs behind v5.0-dev.

What was done?

Merged origin/v5.0-dev (f282f29999) into origin/v5.1-dev (a9336f9486) as a merge commit. Incoming PRs:

Conflicts resolved by hand:

Merge fixes for v5.0-dev code written against APIs that v5.1-dev changed (part of the merge commit):

  • rs-drive-abci/.../update_state_masternode_list/mod.rs (fix(drive-abci)!: keep the operator identity when Core re-encodes the same operator key #5461), consensus code. is_basic_re_encoding_of_legacy_key decoded the stored operator key with blsful's from_bytes_with_mode(.., Legacy). v5.1-dev removed the dpp::bls_signatures re-export (refactor: share rust-dashcore BLS backend through dpp::bls #5320). The port does what blsful did:

    1. Moves the legacy sign flag (bit 7) to the basic-scheme positions (compression bit 7, sign bit 5).
    2. Refuses a legacy encoding with bit 5 or 6 set, except the point at infinity.
    3. Decodes the result with dpp::bls::PublicKey::try_from. Like blsful, that decode checks that x is below the field modulus, that the point is on the curve and that it is in the subgroup, and it accepts the canonical point at infinity.

    dash-pkc's own legacy (Chia) decoder is deliberately not used. It clears stray flag bits instead of refusing them, skips the subgroup check and refuses the point at infinity, so v5.1-dev would have replayed protocol 14 differently from v5.0-dev.

    How the port was checked:

    • All of fix(drive-abci)!: keep the operator identity when Core re-encodes the same operator key #5461's pinned tests pass unchanged: Core's recorded re-encodings, the refused encodings, the point at infinity, a key and its negation, and the recorded listdiff runs.
    • A throwaway differential program compared blsful at 0c34a7a4 with the port on 81,773 inputs: valid keys in both schemes, every first-byte flag variant, random bit flips, 500 on-curve points outside the subgroup in both flag layouts, x = 0, x = p, random bytes and wrong lengths. It found no differences: the same 16,501 legacy encodings were accepted, with byte-identical output. (blsful pins blst =0.3.12 and dash-pkc needs 0.3.17, so the comparison ran as two binaries that exchanged results through a file.)
    • An independent review of the code paths agreed. blst's e1.c, map_to_g1.c and sqrt.c are byte-identical between 0.3.12 and 0.3.17.
  • operator_key_encoding_tests.rs: key_and_negation built the negation with blsful's point negation. It now derives it as the public key of the negated secret (group order minus the secret), which keeps it independent of the encoding logic under test.

  • rs-platform-wallet/src/masternode/locator.rs (fix(platform-wallet): find a masternode's operator identity after Core re-encodes its operator key #5462): the legacy-derived operator identity id uses dpp::bls::PublicKey::try_from(..) and to_legacy_bytes() instead of blsful's Modern decode and Legacy encode. The same differential run covered it: the same 16,530 inputs were accepted, with identical output.

  • rs-platform-wallet/src/manager/load.rs and rs-platform-wallet-ffi/src/persistence.rs (fix(platform-wallet)!: persist DashPay coreHeight backfill coverage so a relaunch resumes instead of rewinding again #5026 tests): ExtendedPubKey::from_priv no longer takes a secp256k1 context (secp256k1 0.33, chore(platform)!: update rust-dashcore (incl secp256k1 0.33) #5307).

Pins and Cargo.lock

  • Cargo.toml: only the workspace version moves (5.0.0-rc.3 → 5.0.0-rc.4).
  • rust-dashcore: v5.1-dev keeps its dev pin, 626c91d1. v5.0-dev's pin (edc4d430) hasn't moved since chore: merge v5.0-dev into v5.1-dev #5432, and the incoming code builds against 626c91d1 after the fixes above, so nothing needs the integration branch.
  • GroveDB: both branches pin fc7028d8.
  • Cargo.lock: kept v5.1-dev's lockfile. The merge changes only the 48 workspace package versions (5.0.0-rc.3 → 5.0.0-rc.4). No crates.io or git package moved, and every build and test ran with --locked.

Protocol version 15

Generated artefacts

None needed regeneration. The incoming PRs change no GroveDB structure: should_match_committed_grovedb_structure_json passes at protocol 15. They also change no wasm-sdk signed-response fixtures, and the wasm-sdk tests pass with the committed ones.

Files changed on both branches and merged automatically (worth a look in review)

  • rs-drive-abci/.../update_masternode_identities/update_operator_identity/v0/mod.rs, .../update_masternode_list/mod.rs and .../update_state_masternode_list/v1/mod.rs: v5.1-dev only moved their tests to dpp::bls (refactor: share rust-dashcore BLS backend through dpp::bls #5320).
  • rs-drive-abci/.../identity_create_from_addresses/tests.rs and .../identity_update/mod.rs
  • rs-platform-wallet: error.rs, manager/accessors.rs, manager/mod.rs, masternode/locator.rs, test_support.rs, wallet/persister.rs, wallet/platform_wallet_traits.rs, wallet/provider_ecdsa_key_tests.rs, wallet/identity/network/{contact_requests,dpns_marketplace,identity_handle,payments}.rs
  • rs-platform-wallet-ffi: manager.rs, persistence.rs
  • yarn.lock, .pnp.cjs and the package.json version bumps

Other effects on v5.1-dev:

Please merge with a merge commit (not squash) so the v5.0-dev history stays shared with v5.1-dev.

How Has This Been Tested?

Run locally on the merged tree:

  • cargo check --workspace --all-targets --locked: passes.
  • cargo clippy --workspace --all-targets --all-features --locked -- --no-deps -D warnings (CI's invocation): passes.
  • cargo fmt --all -- --check: passes.
  • cargo test --all-features --locked --lib --tests with CI's RUST_MIN_STACK=4194304:
    • dpp: 5,773 passed, 0 failed (6 ignored).
    • drive: 4,445 passed, 0 failed (6 ignored). This includes should_match_committed_grovedb_structure_json.
    • drive-abci lib: 4,477 passed, 0 failed (17 ignored). This includes fix(drive-abci)!: keep the operator identity when Core re-encodes the same operator key #5461's operator key encoding tests and the is_basic_re_encoding_of_legacy_key pins.
    • dash-sdk: 575 passed, 0 failed (22 ignored).
    • platform-wallet, platform-wallet-storage and platform-wallet-ffi: 3,056 passed, 0 failed (10 ignored).
  • drive-abci strategy_tests (--all-features, RUST_MIN_STACK=4194304): 115 passed, 0 failed (4 ignored).
  • cargo test -p wasm-sdk --lib --locked: 189 passed, with the committed signed-response fixtures.
  • cargo check -p wasm-sdk -p wasm-dpp2 -p wasm-drive-verify --target wasm32-unknown-unknown --locked: passes.
  • yarn constraints and yarn install --immutable --immutable-cache: both pass, and the tree stays clean.
  • CI's "Detect immutable structure changes" script, run locally against origin/v5.1-dev: no violations.
  • The blsful-vs-port differential described above: 81,773 inputs, 0 differences.

Known limitations

Breaking Changes

The merge adds none of its own. It carries the already-reviewed breaking changes #5461, #5463 and #5026 into v5.1-dev.

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
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

PR Hygiene · c89ff0b

  • Bots — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 5 threads unresolved — resolve them — /skip-bots proceeds without the ones not yet reported
  • Self-review — post /self-reviewed
  • Reviewer requests paused — your 5 review slots are occupied; this PR is excluded from reviewers' queues. Required approvals still count without a slot.
  • Build green
  • Approvals
    • files with no dedicated owner — you own it
    • dashmate — you own it
    • system-contracts — you own it
    • js-wasm-sdk — you own it
    • kotlin-sdk (packages/kotlin-sdk/sdk/schemas/org.dashfoundation.dashsdk.persistence.DashDatabase/15.json, packages/kotlin-sdk/sdk/src/androidTest/kotlin/org/dashfoundation/dashsdk/persistence/DashDatabaseMigrationTest.kt, packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/ffi/NativePersistenceBridge.kt and 6 more) — HashEngineering
    • dpp — you own it
    • rs-drive-abci — you own it
    • rs-drive — you own it
    • rs-platform-wallet-ffi (packages/rs-platform-wallet-ffi/src/contact_persistence.rs, packages/rs-platform-wallet-ffi/src/core_wallet_types.rs, packages/rs-platform-wallet-ffi/src/manager.rs and 2 more) — HashEngineering or ZocoLini or llbartekll or romchornyi
    • wallet-storage (packages/rs-platform-wallet-storage/src/sqlite/persister.rs, packages/rs-platform-wallet-storage/src/sqlite/schema/versions.rs) — lklimek
    • rs-platform-wallet (packages/rs-platform-wallet/src/changeset/changeset.rs, packages/rs-platform-wallet/src/changeset/client_wallet_start_state.rs, packages/rs-platform-wallet/src/changeset/core_bridge.rs and 29 more) — HashEngineering or ZocoLini or llbartekll or romchornyi

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

shumkov and others added 8 commits October 17, 2026 10:02
#5456)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ion 14 (#5463)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…o a relaunch resumes instead of rewinding again (#5026)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Brings in v5.0.0-rc.4 (#5456, #5458, #5463, #5461, #5462, #5480) and
#5026 from v5.0-dev at f282f29.

Conflicts:
- CHANGELOG.md: [Unreleased] stays on top, followed by the rc.4 entry.
- rs-platform-wallet/src/changeset/core_bridge.rs: keep v5.1-dev's
  WalletEvent::ChainTruncated arm and take #5026's SyncHeightAdvanced
  arm, which drops a watermark queued above the in-memory cursor.

Fixes for v5.0-dev code written against APIs v5.1-dev changed:
- update_state_masternode_list (#5461): the legacy-to-basic operator key
  check used blsful's legacy decoder, whose dpp::bls_signatures re-export
  v5.1-dev removed (#5320). It now moves the legacy flags to their basic
  positions as blsful did (refusing bits 5 and 6) and decodes the result
  with dpp::bls::PublicKey, which checks the field bound, the curve and
  the subgroup as blsful did. dash-pkc's own legacy decoder is not used:
  it clears stray flag bits, skips the subgroup check and refuses the
  point at infinity. A throwaway differential run against blsful over
  81,773 inputs (valid keys in both schemes, flag variants, bit flips,
  500 on-curve points outside the subgroup in both flag layouts, random
  and edge bytes) gave identical results. The test helper builds a key's
  negation from the negated secret.
- masternode/locator.rs (#5462): the basic-to-legacy id uses
  dpp::bls::PublicKey::to_legacy_bytes; same differential run.
- manager/load.rs and platform-wallet-ffi persistence.rs (#5026):
  ExtendedPubKey::from_priv no longer takes a secp256k1 context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shumkov shumkov added this to the v5.1.0 milestone Oct 17, 2026
@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f7a86b2c-91ad-46fa-b7e6-c693b79b82cf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 5 threads unresolved — resolve them. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 17, 2026
@thepastaclaw

thepastaclaw commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 5 blocking finding(s) (commit c89ff0b) · triage: critical

@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 1 + Phase 2

Static verification of c89ff0b confirms five blocking issues: two changes to shipped consensus generations without PlatformVersion activation, an unsafe truncation/backfill restart sequence, and two binding-layer ownership violations. The PV14 hash-key versioning finding is not retained because that generation belongs only to unreleased protocols. Current CI shows Rust workspace and native SDK checks passing, with one browser suite still pending; no local builds or tests were run.

🔴 5 blocking | 🟡 5 suggestion(s)

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 6: gemini-3.8-flash-high (agent: phase1-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The large, intricate diff changes consensus-critical operator-key decoding in update_state_masternode_list/mod.rs, protocol-14 identity-key validation, and persisted wallet state with database migrations.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 71% left, 5h 100% left
  • 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; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — tenderdash-consensus-security (completed, effort xhigh); 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-platform-wallet/src/wallet/identity/network/payments.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/identity/network/payments.rs:380-389: Account for chain-truncation rewinds before persisting coverage
  The merge makes the missing rewind bookkeeping in PlatformWalletInfo::truncate_above observable through durable coverage today, independently of #5380. For example, a permitted truncation from 1000 to the last chain lock at 999 lowers the live cursor but leaves the host cursor at 1000. Registering a receival contact with checkpoint 999 then takes the forward-covered path: floor and owed are both None, so this round persists coverage without lowering the host cursor. After restart, the restored coverage suppresses reconciliation's rewind, while the pinned dash-spv scanner skips the range through the restored cursor, although the new account was never scanned through block 1000. Previously the process-local coverage guard disappeared on restart and the contact triggered a backfill. Route successful cursor lowering in truncate_above through the same barrier and cursor-debt bookkeeping as account additions and manual resets. Add a truncation → contact registration → record-only reconciliation → restart regression, including advances queued before truncation; dropping queued heights above the current cursor does not protect this record-only path.

In `packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/v0/mod.rs:130-138: Keep the Core-V24 consensus change out of shipped list generation v0
  Protocols 1–13 still select update_masternode_list generation 0. Once Core V24 is active, this new call removes an operator-key update that the previous binary applied, preserving different stored bytes and a different operator identity under the same Platform protocol. The Core-height gate makes upgraded nodes deterministic and preserves earlier history, but it does not make the edit inert: the new tests exercise the changed result at PV13, and Core activation does not advance PlatformVersion. The book explicitly requires a new generation when a runtime-gated edit can change shipped behavior. Preserve the old v0 path and activate normalization through a new list-update generation selected only by unreleased protocol tables. Coordinate that correction with v5.0-dev so the branches do not acquire different rules for the same protocol; a deployment instruction to upgrade every node before Core activation is not the required version boundary.

In `packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs`:
- [BLOCKING] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_identities/update_operator_identity/v0/mod.rs:214-218: Version activation of the new masternode key operation
  The replacement is not consensus-equivalent in this shipped generation. When a new payout key's hash already exists in the unique-key index, the previous AddNewKeysToIdentity path returns UniqueKeyAlreadyExists; AddNewKeysToMasternodeIdentity instead omits the non-unique reference and successfully adds the key. A masternode can reach this branch by changing its payout address to the HASH160 of an already registered user-identity public key, including at PV13 without Core V24 activation. The new Drive dispatcher reuses add_new_keys_to_identity: 0, so it supplies no activation boundary. Preserve the old operation selection in shipped operator and owner-withdrawal generations, introduce new generations selected only by unreleased tables, and independently version the new Drive method. Matching historical blocks or comparing operations only for non-colliding hashes does not establish agreement on this reachable collision branch. Carry the same correction to the source branch to preserve cross-branch protocol consistency.

In `packages/rs-unified-sdk-jni/src/persistence.rs`:
- [BLOCKING] packages/rs-unified-sdk-jni/src/persistence.rs:2921-2929: Use the wallet-owned backfill decoder instead of duplicating it in JNI
  JNI introduces a second implementation of the persisted coverage format: it defines the 72-byte entry width, validates blob alignment, chooses to discard malformed records, and decodes each field. The wallet already owns this exact codec and malformed-blob contract in DashPayBackfillRecord::from_parts, with covered_bytes as its inverse; the JNI write trampoline independently reproduces that packing too. This violates the project's single-owner validation boundary and makes a future format change require synchronized native and Android implementations. Keep JVM field access and numeric representation checks at the boundary, but expose an owner-backed conversion through platform-wallet-ffi and delegate both blob decoding and encoding to it. The existing FFI restore path can still perform canonicalization; the problem here is the additional format validation and admission implementation, not a claim that Android currently skips canonicalization.

In `packages/rs-platform-wallet-ffi/src/persistence.rs`:
- [BLOCKING] packages/rs-platform-wallet-ffi/src/persistence.rs:2307-2312: Keep backfill coverage policy in platform-wallet rather than the FFI persister
  This filter distinguishes an empty invalidation from a covering record and decides whether Rust-emitted state is delivered based on atomicity, cursor presence, and cursor callback delivery. A withheld record does not itself fail the store. Those are coverage eligibility and invalidation decisions, not C representation conversion, and platform-wallet already owns them through persists_backfill_coverage and settle_published_backfill. The book permits bindings to persist and marshal Rust-owned state, not to implement a second version of its safety policy. Keep callback availability and delivery results in the adapter, but centralize the record-delivery decision in the wallet owner using those facts. The adapter should then marshal the selected record or report that a required write could not be completed, rather than silently choosing a binding-specific subset.

In `packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs`:
- [SUGGESTION] packages/rs-drive-abci/src/execution/platform_events/core_based_updates/update_masternode_list/update_state_masternode_list/mod.rs:170-172: Test subgroup rejection against the matching basic re-encoding
  The two x=0 inputs do not currently demonstrate subgroup rejection. The loop compares each against an unrelated valid key and its unchanged legacy bytes, so a decoder without subgroup validation would still return false because neither comparison matches its translated basic encoding. Add comparisons against 0x80 followed by zeros for the null legacy encoding and 0xa0 followed by zeros for its sign-set counterpart. These assertions specifically detect removal of the subgroup check on which this consensus decoder port relies.

In `packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs:727-730: Carry the wallet API migration into Unreleased release notes
  The new fields enlarge the host-allocated WalletRestoreEntryFFI array element, and ContactRequestFFI explicitly grows from 200 to 208 bytes. Size negotiation on PersistenceCallbacksExtension protects the callback extension, not the allocation size or stride of these other structs, so C consumers must rebuild against matching headers. CHANGELOG.md retains the rc.4 entry but has no Unreleased entry for #5026, which landed afterward. Record the rebuild requirement there alongside the Room schema 15 migration and affected public Rust enum matches, so consumers do not mistake callback compatibility for compatibility of all persistence structures.

In `packages/rs-platform-wallet/src/changeset/core_bridge.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/changeset/core_bridge.rs:396-405: Track a producer-owned scan epoch upstream
  At both the pinned rust-dashcore revision and its supplied default tip, SyncHeightAdvanced contains only wallet_id and height. Platform therefore reconstructs scan identity by counting update_synced_height hooks, consuming events in order, and carrying counters across same-id wallet replacements. The pinned process_block implementation supports the current hook/emission pairing, but this is a workaround for missing producer-owned event identity in dashpay/rust-dashcore, not a responsibility every persistence consumer should reproduce. Track an upstream change that attaches a scan/lifecycle epoch to cursor events and advances it through the authoritative rewind/reset APIs. Link that issue or PR here; #649's scan-certification safeguards and the existing resync/recovery work do not provide cursor-event identity.

In `packages/rs-platform-wallet/src/manager/load.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/load.rs:270-272: Skip already-loaded wallets before validating every external address
  The new external-pool validation runs before the existing-wallet check at the publication step. The loader is deliberately called again on SDK reactivation and StateFlow re-emission, so a call that ultimately skips an already-loaded wallet still re-derives every address in its marked external pools. pool_derives_from performs synchronous derive_address_at work for each restored row on the async worker. Add an early existing-wallet check before this validation while retaining the authoritative publication-time check for concurrent loaders. The existing pool-validation counter can assert that an idempotent repeated load performs no derivation.

In `packages/rs-platform-wallet/src/masternode/locator.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/masternode/locator.rs:395-402: Avoid pushing a duplicate operator identifier for identical encodings
  The DPP PublicKey facade accepts canonical infinity and returns the same bytes from to_legacy_bytes. For that input, this unconditional push adds the literal operator identifier twice. first_operator_identity iterates candidates without deduplication, so a missing identity causes the same proof-backed fetch to run twice. Append the legacy candidate only when its bytes differ from the input.

Comment on lines +380 to +389
let needed_cursor = match (floor, owed) {
(Some(floor), Some(owed)) => Some(floor.min(owed)),
(Some(floor), None) => Some(floor),
(None, owed) => owed,
};
// Never write a cursor at or above the one the host already holds:
// coverage is safe beside any lower durable cursor, and writing a
// higher one would claim rows the adapter has not persisted yet.
let cursor_to_write =
needed_cursor.filter(|cursor| durable.is_none_or(|durable| *cursor < durable.height));

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: Account for chain-truncation rewinds before persisting coverage

The merge makes the missing rewind bookkeeping in PlatformWalletInfo::truncate_above observable through durable coverage today, independently of #5380. For example, a permitted truncation from 1000 to the last chain lock at 999 lowers the live cursor but leaves the host cursor at 1000. Registering a receival contact with checkpoint 999 then takes the forward-covered path: floor and owed are both None, so this round persists coverage without lowering the host cursor. After restart, the restored coverage suppresses reconciliation's rewind, while the pinned dash-spv scanner skips the range through the restored cursor, although the new account was never scanned through block 1000. Previously the process-local coverage guard disappeared on restart and the contact triggered a backfill. Route successful cursor lowering in truncate_above through the same barrier and cursor-debt bookkeeping as account additions and manual resets. Add a truncation → contact registration → record-only reconciliation → restart regression, including advances queued before truncation; dropping queued heights above the current cursor does not protect this record-only path.

source: gemini-3.8-flash-high (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general)

Comment on lines +130 to +138
if !start_from_scratch {
// Protocol versions 1 to 13 select this generation. The call changes no entry unless
// DEPLOYMENT_V24 is active at `core_block_height`, so blocks at earlier Core heights
// execute unchanged.
self.drop_unchanged_operator_keys(
state.full_masternode_list(),
core_block_height,
&mut masternode_diff.updated_mns,
)?;

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: Keep the Core-V24 consensus change out of shipped list generation v0

Protocols 1–13 still select update_masternode_list generation 0. Once Core V24 is active, this new call removes an operator-key update that the previous binary applied, preserving different stored bytes and a different operator identity under the same Platform protocol. The Core-height gate makes upgraded nodes deterministic and preserves earlier history, but it does not make the edit inert: the new tests exercise the changed result at PV13, and Core activation does not advance PlatformVersion. The book explicitly requires a new generation when a runtime-gated edit can change shipped behavior. Preserve the old v0 path and activate normalization through a new list-update generation selected only by unreleased protocol tables. Coordinate that correction with v5.0-dev so the branches do not acquire different rules for the same protocol; a deployment instruction to upgrade every node before Core activation is not the required version boundary.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor, tenderdash-consensus-security)

Comment on lines 214 to 218
if !non_unique_keys_to_add.is_empty() {
drive_operations.push(IdentityOperation(AddNewKeysToIdentity {
drive_operations.push(IdentityOperation(AddNewKeysToMasternodeIdentity {
identity_id: operator_identifier.to_buffer(),
unique_keys_to_add: vec![],
non_unique_keys_to_add,
}));

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: Version activation of the new masternode key operation

The replacement is not consensus-equivalent in this shipped generation. When a new payout key's hash already exists in the unique-key index, the previous AddNewKeysToIdentity path returns UniqueKeyAlreadyExists; AddNewKeysToMasternodeIdentity instead omits the non-unique reference and successfully adds the key. A masternode can reach this branch by changing its payout address to the HASH160 of an already registered user-identity public key, including at PV13 without Core V24 activation. The new Drive dispatcher reuses add_new_keys_to_identity: 0, so it supplies no activation boundary. Preserve the old operation selection in shipped operator and owner-withdrawal generations, introduce new generations selected only by unreleased tables, and independently version the new Drive method. Matching historical blocks or comparing operations only for non-colliding hashes does not establish agreement on this reachable collision branch. Carry the same correction to the source branch to preserve cross-branch protocol consistency.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor, tenderdash-consensus-security)

Comment on lines +2921 to +2929
if floor < 0
|| rewound_from < 0
|| !blob
.len()
.is_multiple_of(DASHPAY_BACKFILL_COVERED_ENTRY_LEN)
{
log::warn!(
"load: malformed DashPay backfill record on the wallet row (floor={floor}, \
rewound_from={rewound_from}, blob_len={}); reading as no record",

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: Use the wallet-owned backfill decoder instead of duplicating it in JNI

JNI introduces a second implementation of the persisted coverage format: it defines the 72-byte entry width, validates blob alignment, chooses to discard malformed records, and decodes each field. The wallet already owns this exact codec and malformed-blob contract in DashPayBackfillRecord::from_parts, with covered_bytes as its inverse; the JNI write trampoline independently reproduces that packing too. This violates the project's single-owner validation boundary and makes a future format change require synchronized native and Android implementations. Keep JVM field access and numeric representation checks at the boundary, but expose an owner-backed conversion through platform-wallet-ffi and delegate both blob decoding and encoding to it. The existing FFI restore path can still perform canonicalization; the problem here is the additional format validation and admission implementation, not a claim that Android currently skips canonicalization.

source: gpt-6.1-sol (phase2-reviewer: architecture-layering)

Comment on lines +2307 to +2312
.as_ref()
.is_some_and(|core| core.synced_height.is_some());
if let Some(record) = changeset.dashpay_backfill.as_ref().filter(|record| {
record.is_empty()
|| (host_stores_cursors && (!backfill_cursor_owed || cursor_delivered))
}) {

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: Keep backfill coverage policy in platform-wallet rather than the FFI persister

This filter distinguishes an empty invalidation from a covering record and decides whether Rust-emitted state is delivered based on atomicity, cursor presence, and cursor callback delivery. A withheld record does not itself fail the store. Those are coverage eligibility and invalidation decisions, not C representation conversion, and platform-wallet already owns them through persists_backfill_coverage and settle_published_backfill. The book permits bindings to persist and marshal Rust-owned state, not to implement a second version of its safety policy. Keep callback availability and delivery results in the adapter, but centralize the record-delivery decision in the wallet owner using those facts. The adapter should then marshal the selected record or report that a required write could not be completed, rather than silently choosing a binding-specific subset.

source: gpt-6.1-sol (phase2-reviewer: ffi-engineer)

Comment on lines +170 to +172
let null_key = vec![0u8; 48];
let mut sign_set_x_zero = vec![0u8; 48];
sign_set_x_zero[0] = 0x80;

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: Test subgroup rejection against the matching basic re-encoding

The two x=0 inputs do not currently demonstrate subgroup rejection. The loop compares each against an unrelated valid key and its unchanged legacy bytes, so a decoder without subgroup validation would still return false because neither comparison matches its translated basic encoding. Add comparisons against 0x80 followed by zeros for the null legacy encoding and 0xa0 followed by zeros for its sign-set counterpart. These assertions specifically detect removal of the subgroup check on which this consensus decoder port relies.

Suggested change
let null_key = vec![0u8; 48];
let mut sign_set_x_zero = vec![0u8; 48];
sign_set_x_zero[0] = 0x80;
let null_key = vec![0u8; 48];
let mut sign_set_x_zero = vec![0u8; 48];
sign_set_x_zero[0] = 0x80;
assert!(!is_basic_re_encoding_of_legacy_key(
&null_key,
&sign_set_x_zero
));
let mut basic_negative_x_zero = vec![0u8; 48];
basic_negative_x_zero[0] = 0xa0;
assert!(!is_basic_re_encoding_of_legacy_key(
&sign_set_x_zero,
&basic_negative_x_zero
));

source: gpt-6.1-sol (phase2-reviewer: rust-quality)

Comment on lines +727 to +730
pub has_dashpay_backfill: bool,
/// Lowest height the backfill rewound the cursor to.
pub dashpay_backfill_floor: u32,
/// Highest cursor the backfill rewound from — the height the scan climbs

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: Carry the wallet API migration into Unreleased release notes

The new fields enlarge the host-allocated WalletRestoreEntryFFI array element, and ContactRequestFFI explicitly grows from 200 to 208 bytes. Size negotiation on PersistenceCallbacksExtension protects the callback extension, not the allocation size or stride of these other structs, so C consumers must rebuild against matching headers. CHANGELOG.md retains the rc.4 entry but has no Unreleased entry for #5026, which landed afterward. Record the rebuild requirement there alongside the Room schema 15 migration and affected public Rust enum matches, so consumers do not mistake callback compatibility for compatibility of all persistence structures.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor, tenderdash-consensus-security)

Comment on lines +396 to +405
/// The barrier counts instead. The engine emits a `SyncHeightAdvanced` only
/// right after calling this wallet's `update_synced_height` hook, under the
/// same manager write lock, so the hook counts emissions exactly
/// ([`note_emitted`](Self::note_emitted)), and the adapter counts them back
/// down as it projects them ([`take_next`](Self::take_next)). A rewind —
/// made under that same write lock — snapshots how many are still in flight
/// ([`arm`](Self::arm)): exactly those were emitted before it, and the
/// adapter drops them as they arrive. Every later advance is tagged with the
/// rewind epoch it was projected in, and a commit strips one whose epoch a
/// rewind has since superseded, which covers an advance the adapter had

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: Track a producer-owned scan epoch upstream

At both the pinned rust-dashcore revision and its supplied default tip, SyncHeightAdvanced contains only wallet_id and height. Platform therefore reconstructs scan identity by counting update_synced_height hooks, consuming events in order, and carrying counters across same-id wallet replacements. The pinned process_block implementation supports the current hook/emission pairing, but this is a workaround for missing producer-owned event identity in dashpay/rust-dashcore, not a responsibility every persistence consumer should reproduce. Track an upstream change that attaches a scan/lifecycle epoch to cursor events and advances it through the authoritative rewind/reset APIs. Link that issue or PR here; #649's scan-certification safeguards and the existing resync/recovery work do not provide cursor-event identity.

source: gpt-6.1-sol (phase2-reviewer: architecture-layering)

Comment on lines +270 to +272
if crate::changeset::persists_outbound_account_markers(&*self.persister) {
let distrusted =
distrust_markers_of_foreign_external_pools(&wallet, &mut platform_info);

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: Skip already-loaded wallets before validating every external address

The new external-pool validation runs before the existing-wallet check at the publication step. The loader is deliberately called again on SDK reactivation and StateFlow re-emission, so a call that ultimately skips an already-loaded wallet still re-derives every address in its marked external pools. pool_derives_from performs synchronous derive_address_at work for each restored row on the async worker. Add an early existing-wallet check before this validation while retaining the authoritative publication-time check for concurrent loaders. The existing pool-validation counter can assert that an idempotent repeated load performs no derivation.

source: gpt-6.1-sol (phase2-reviewer: rust-quality)

Comment on lines +395 to +402
if let Ok(legacy) =
BlsPublicKey::try_from(operator_public_key.as_slice()).and_then(|key| key.to_legacy_bytes())
{
ids.push(Identifier::create_operator_identifier(
pro_tx_hash_display,
&legacy,
));
}

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: Avoid pushing a duplicate operator identifier for identical encodings

The DPP PublicKey facade accepts canonical infinity and returns the same bytes from to_legacy_bytes. For that input, this unconditional push adds the literal operator identifier twice. first_operator_identity iterates candidates without deduplication, so a missing identity causes the same proof-backed fetch to run twice. Append the legacy candidate only when its bytes differ from the input.

Suggested change
if let Ok(legacy) =
BlsPublicKey::try_from(operator_public_key.as_slice()).and_then(|key| key.to_legacy_bytes())
{
ids.push(Identifier::create_operator_identifier(
pro_tx_hash_display,
&legacy,
));
}
if let Ok(legacy) =
BlsPublicKey::try_from(operator_public_key.as_slice()).and_then(|key| key.to_legacy_bytes())
{
if &legacy != operator_public_key {
ids.push(Identifier::create_operator_identifier(
pro_tx_hash_display,
&legacy,
));
}
}

source: gemini-3.8-flash-high (phase1-reviewer: general, rust-quality)

This branch has not been deployed

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

Labels

waiting-bots Waiting for the review bots to report on this head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants