Skip to content

feat(sdk): add address nonce caching and stale-marking on broadcast failure#3409

Draft
lklimek wants to merge 1 commit intov3.1-devfrom
feat/sdk-address-nonce-cache
Draft

feat(sdk): add address nonce caching and stale-marking on broadcast failure#3409
lklimek wants to merge 1 commit intov3.1-devfrom
feat/sdk-address-nonce-cache

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented Mar 26, 2026

Issue being fixed or feature implemented

Closes #3407

The SDK's NonceCache only tracks identity and identity-contract nonces. Address-based transitions (Shield, AddressFundsTransfer, AddressCreditWithdrawal, etc.) return None for owner_id(), so the broadcast failure path in broadcast.rs never triggers any nonce cleanup. Address nonces were fetched fresh from Platform every time with no caching and no stale-marking — meaning address-based transitions had zero parity with the existing identity nonce mechanism.

What was done?

Added address nonce caching to the Rust SDK, achieving parity with the existing identity nonce cache:

  1. NonceCache (internal_cache/mod.rs) — Added address_nonces LRU cache field. New get_address_nonce() method reuses the generic get_or_fetch_nonce() (same staleness, drift, and max-merge logic as identity nonces). New refresh_address() marks a single address entry as stale.

  2. Sdk (sdk.rs) — Public get_address_nonce(address, bump_first, settings) (casts internal u64 → AddressNonce u32) and refresh_address_nonce(address) wrappers.

  3. Broadcast error handler (broadcast.rs) — On broadcast failure, now iterates self.inputs() and calls refresh_address_nonce() for each address — parallel to the existing refresh_identity_nonce() path.

  4. fetch_inputs_with_nonce() (address_inputs.rs) — Now uses the cached nonce via sdk.get_address_nonce(*address, true, None) with bump_first=true, matching the identity nonce pattern. Removed the separate nonce_inc() function (bump now happens inside the cache).

  5. Callers — Updated 4 files (shield.rs, transfer_address_funds.rs, address_credit_withdrawal.rs, top_up_identity_from_addresses.rs) to remove the nonce_inc() wrapper.

How Has This Been Tested?

5 new unit tests in nonce_cache_tests:

  • address_nonce_basic_fetch — empty cache fetches from Platform, returns correct nonce
  • address_nonce_cache_hit — fresh cached value served without Platform fetch
  • address_nonce_refresh_marks_stale — refresh sets timestamp to STALE_TIMESTAMP
  • address_nonce_refresh_preserves_cached_value — stale marking preserves cached nonce for max() merge (critical for preventing "tx already exists in cache" errors)
  • address_nonce_different_addresses_isolated — refresh only affects the targeted address

Build note: The base branch (v3.1-dev) has a pre-existing dependency resolution failure (key-wallet-manager crate missing from rust-dashcore at rev 8cbae416). This prevents cargo check/cargo test from running and is not caused by these changes.

Breaking Changes

None. The public API is only extended (new get_address_nonce and refresh_address_nonce methods on Sdk). The internal nonce_inc() function was removed but was pub(crate) only.

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

🤖 Co-authored by Claudius the Magnificent AI Agent

…ailure

Add address nonce caching parity with identity nonces. Previously,
address-based transitions (Shield, AddressFundsTransfer, etc.) always
fetched nonces fresh from Platform and had no stale-marking on broadcast
failure, because their owner_id() returns None.

Changes:
- Add address_nonces LRU cache to NonceCache, mirroring the existing
  identity and contract nonce caches
- Add get_address_nonce() and refresh_address() methods to NonceCache
- Add get_address_nonce() and refresh_address_nonce() public methods
  to Sdk
- Modify fetch_inputs_with_nonce() to use cached nonces with bump
  instead of raw Platform values, removing the external nonce_inc()
  wrapper that all callers previously applied
- Update broadcast error handler to refresh address nonces on failure,
  matching the existing identity nonce refresh pattern
- Add unit tests for address nonce cache operations

Closes #3407

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Draft detected.

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a97e28f9-e7b5-42bb-82e4-d282d77951e1

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-address-nonce-cache

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 and usage tips.

@lklimek lklimek requested a review from Copilot March 26, 2026 15:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds nonce caching and stale-marking behavior for address-based transitions, bringing them in parity with existing identity nonce caching and improving broadcast-failure recovery.

Changes:

  • Introduced an address nonce LRU in NonceCache, plus SDK wrappers to get/refresh address nonces.
  • Updated broadcast failure handling to mark input-address nonces as stale.
  • Refactored address-based transition builders to rely on cached nonce bumping (removed nonce_inc()).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/rs-sdk/src/sdk.rs Exposes get_address_nonce / refresh_address_nonce as public SDK APIs.
packages/rs-sdk/src/internal_cache/mod.rs Adds address nonce cache, fetch logic, stale refresh, and unit tests.
packages/rs-sdk/src/platform/transition/broadcast.rs Marks address nonces stale on broadcast failure based on transition inputs.
packages/rs-sdk/src/platform/transition/address_inputs.rs Switches nonce assignment to cached SDK method and removes nonce_inc().
packages/rs-sdk/src/platform/transition/shield.rs Removes nonce_inc() usage; relies on fetch_inputs_with_nonce.
packages/rs-sdk/src/platform/transition/transfer_address_funds.rs Removes nonce_inc() usage; relies on fetch_inputs_with_nonce.
packages/rs-sdk/src/platform/transition/address_credit_withdrawal.rs Removes nonce_inc() usage; relies on fetch_inputs_with_nonce.
packages/rs-sdk/src/platform/transition/top_up_identity_from_addresses.rs Removes nonce_inc() usage; relies on fetch_inputs_with_nonce.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let info = ensure_address_exists(&address_infos, *address)?;
ensure_address_balance(*address, info.balance, *amount)?;
inputs_with_nonce.insert(*address, (info.nonce, *amount));
let nonce = sdk.get_address_nonce(*address, true, None).await?;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

fetch_inputs_with_nonce() already fetches AddressInfo for all addresses in one batch (AddressInfo::fetch_many). Calling sdk.get_address_nonce() inside the loop can trigger an additional Platform fetch per address on a cold cache (N+1 network calls) and ignores the already-fetched info.nonce. A concrete fix is to prime/update the nonce cache using info.nonce from address_infos (while still applying the same bump/merge/staleness rules), so the function doesn’t need any extra Platform requests beyond the initial bulk fetch.

Suggested change
let nonce = sdk.get_address_nonce(*address, true, None).await?;
let nonce = info.nonce;

Copilot uses AI. Check for mistakes.
.nonce_cache
.get_address_nonce(self, address, bump_first, &settings)
.await?;
Ok(nonce as AddressNonce)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

Casting nonce (u64) to AddressNonce (u32) via as can silently truncate once the nonce exceeds u32::MAX, producing incorrect nonces. Use a checked conversion (e.g., u32::try_from(nonce) or an equivalent AddressNonce conversion) and return an error on overflow so callers don’t broadcast transitions with corrupted nonces.

Suggested change
Ok(nonce as AddressNonce)
let nonce = AddressNonce::try_from(nonce)?;
Ok(nonce)

Copilot uses AI. Check for mistakes.
address: PlatformAddress,
bump_first: bool,
settings: &PutSettings,
) -> Result<IdentityNonce, Error> {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

get_address_nonce returns IdentityNonce, which is semantically misleading for an address-nonce API and makes it easier to accidentally mix nonce domains. Consider changing the return type to a neutral integer type used by get_or_fetch_nonce (e.g., u64) or to an address-specific nonce type, and keep the address-specific conversion at the boundary (SDK public API) with a checked conversion.

Suggested change
) -> Result<IdentityNonce, Error> {
) -> Result<u64, Error> {

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(sdk): auto-retry on AddressInvalidNonceError during state transition broadcast

2 participants