feat(sdk): add address nonce caching and stale-marking on broadcast failure#3409
feat(sdk): add address nonce caching and stale-marking on broadcast failure#3409
Conversation
…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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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?; |
There was a problem hiding this comment.
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.
| let nonce = sdk.get_address_nonce(*address, true, None).await?; | |
| let nonce = info.nonce; |
| .nonce_cache | ||
| .get_address_nonce(self, address, bump_first, &settings) | ||
| .await?; | ||
| Ok(nonce as AddressNonce) |
There was a problem hiding this comment.
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.
| Ok(nonce as AddressNonce) | |
| let nonce = AddressNonce::try_from(nonce)?; | |
| Ok(nonce) |
| address: PlatformAddress, | ||
| bump_first: bool, | ||
| settings: &PutSettings, | ||
| ) -> Result<IdentityNonce, Error> { |
There was a problem hiding this comment.
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.
| ) -> Result<IdentityNonce, Error> { | |
| ) -> Result<u64, Error> { |
Issue being fixed or feature implemented
Closes #3407
The SDK's
NonceCacheonly tracks identity and identity-contract nonces. Address-based transitions (Shield,AddressFundsTransfer,AddressCreditWithdrawal, etc.) returnNoneforowner_id(), so the broadcast failure path inbroadcast.rsnever 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:
NonceCache(internal_cache/mod.rs) — Addedaddress_noncesLRU cache field. Newget_address_nonce()method reuses the genericget_or_fetch_nonce()(same staleness, drift, and max-merge logic as identity nonces). Newrefresh_address()marks a single address entry as stale.Sdk(sdk.rs) — Publicget_address_nonce(address, bump_first, settings)(casts internal u64 → AddressNonce u32) andrefresh_address_nonce(address)wrappers.Broadcast error handler (
broadcast.rs) — On broadcast failure, now iteratesself.inputs()and callsrefresh_address_nonce()for each address — parallel to the existingrefresh_identity_nonce()path.fetch_inputs_with_nonce()(address_inputs.rs) — Now uses the cached nonce viasdk.get_address_nonce(*address, true, None)withbump_first=true, matching the identity nonce pattern. Removed the separatenonce_inc()function (bump now happens inside the cache).Callers — Updated 4 files (
shield.rs,transfer_address_funds.rs,address_credit_withdrawal.rs,top_up_identity_from_addresses.rs) to remove thenonce_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 nonceaddress_nonce_cache_hit— fresh cached value served without Platform fetchaddress_nonce_refresh_marks_stale— refresh sets timestamp toSTALE_TIMESTAMPaddress_nonce_refresh_preserves_cached_value— stale marking preserves cached nonce formax()merge (critical for preventing "tx already exists in cache" errors)address_nonce_different_addresses_isolated— refresh only affects the targeted addressBuild note: The base branch (
v3.1-dev) has a pre-existing dependency resolution failure (key-wallet-managercrate missing fromrust-dashcoreat rev8cbae416). This preventscargo check/cargo testfrom running and is not caused by these changes.Breaking Changes
None. The public API is only extended (new
get_address_nonceandrefresh_address_noncemethods onSdk). The internalnonce_inc()function was removed but waspub(crate)only.Checklist:
For repository code-owners and collaborators only
🤖 Co-authored by Claudius the Magnificent AI Agent