Skip to content

Conversation

@levonpetrosyan93
Copy link
Contributor

@levonpetrosyan93 levonpetrosyan93 commented Sep 29, 2025

In Spark state we keep used coin tags inside a unordered_map,
For mobile we have an api to get that container in sectors, as the order is not predefined and can differ across servers,
We had sync issues, on mobile.
To solve that we add one more container with fixed order, in case -mobile flag is passed, and we use that container in RPC call.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 29, 2025

Walkthrough

Refactors RPC getusedcoinstags to read mobile-specific spend tags. Adds mobileUsedLTags storage, manages it in CSparkState.Reset/AddSpend/RemoveSpend, and exposes GetSpendsMobile(). RPC iteration/storage uses std::vector<std::pair<GroupElement,int>> while element serialization format is unchanged.

Changes

Cohort / File(s) Summary
RPC: mobile spend tag source
src/rpc/misc.cpp
Refactor getusedcoinstags to call sparkState->GetSpendsMobile() and iterate a std::vector<std::pair<GroupElement,int>> instead of an unordered_map; per-element serialization unchanged.
Spark state: implementation
src/spark/state.cpp
Add/maintain mobileUsedLTags: cleared in Reset, recorded in AddSpend when mobile flag is set, erased in RemoveSpend when mobile flag is set; new accessor exposes mobile spends.
Spark state: API and storage
src/spark/state.h
Add std::vector<std::pair<GroupElement,int>> mobileUsedLTags (private) and public GetSpendsMobile() const accessor returning a const reference.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant RPC as getusedcoinstags
  participant State as CSparkState

  Client->>RPC: Request used coin tags (mobile)
  RPC->>State: GetSpendsMobile()
  State-->>RPC: vector<(GroupElement,int)>
  RPC-->>Client: Serialized tags

  rect rgba(230,240,255,0.6)
    note over State: Spend lifecycle updates mobileUsedLTags
    participant Wallet
    Wallet->>State: AddSpend(tag, mobile=true)
    State-->>State: mobileUsedLTags.push_back((tag,idx))
    Wallet->>State: RemoveSpend(tag, mobile=true)
    State-->>State: erase tag from mobileUsedLTags
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • psolstice

Poem

I thump my paws on mobile trails,
New tags hop in tidy vector bales.
Reset, add, remove—swift and small,
RPCs fetch them, one and all.
Carrots counted, hops in rows 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title uses a vague term “fixed” and does not specify the change to support a fixed‐order mobile container in the getusedcoinstags RPC, making it unclear what was modified. Please revise the title to clearly describe the main change, for example: “Use fixed‐order mobile container in getusedcoinstags RPC” so that readers understand the update at a glance.
Description Check ⚠️ Warning The pull request description does not follow the repository’s template because it lacks the required “## PR intention” and “## Code changes brief” headings and structure, making it unclear which section addresses the mandatory intent versus the brief summary of changes. Please update the description by adding the “## PR intention” section to clearly state the purpose and issue addressed by the PR and optionally include a “## Code changes brief” section to outline architectural or UX changes as per the template.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch getusedcoinstags

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 784d071 and ba90182.

📒 Files selected for processing (1)
  • src/spark/state.cpp (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/spark/state.cpp (1)
src/spark/primitives.cpp (2)
  • tag (43-49)
  • tag (43-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: build-guix-aarch64-linux-gnu
  • GitHub Check: build-guix-x86_64-w64-mingw32
  • GitHub Check: build-guix-x86_64-linux-gnu
  • GitHub Check: build-mac-cmake
  • GitHub Check: build-guix-x86_64-apple-darwin
  • GitHub Check: build-guix-arm64-apple-darwin
  • GitHub Check: build-windows-cmake
  • GitHub Check: build-linux-cmake
  • GitHub Check: Create Source Distribution
  • GitHub Check: Build for Linux (CMake)
  • GitHub Check: Build for macOS (CMake)
  • GitHub Check: Build for Windows (CMake)
  • GitHub Check: Create Source Distribution
🔇 Additional comments (4)
src/spark/state.cpp (4)

959-959: LGTM!

The clear operation correctly resets the mobile-specific state alongside the regular state containers.


1095-1097: LGTM!

The mobile-specific tracking is correctly placed inside the mintMetaInfo conditional check, ensuring consistency between usedLTags and mobileUsedLTags. The vector append preserves insertion order, which is essential for mobile client synchronization.


1108-1115: Addresses past review feedback correctly.

The mobile logic now executes only when iter != usedLTags.end(), avoiding the loop when the tag is not in state. The linear search through mobileUsedLTags is O(n), but this is an acceptable tradeoff given:

  1. Removal occurs only during block disconnection or reorgs (rare operations)
  2. Vector maintains insertion order required for mobile client synchronization
  3. Alternative structures would add complexity without significant benefit

Based on learnings


1494-1496: LGTM!

The getter provides read-only access to the mobile-specific spends collection, following the same pattern as the existing GetSpends() method.


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/spark/state.h (2)

240-241: Add thread-safety note for accessor.

GetSpendsMobile() exposes internal state; callers must hold cs_main while accessing/copying to avoid races. Please add a brief comment mirroring other getters.


267-269: Consider determinism and removal cost trade-offs.

Storing a second structure is fine for mobile, but be aware:

  • Order depends on insertion sites; ensure upstream insertion order is deterministic network-wide.
  • RemoveSpend is O(N). Acceptable for rare reorgs; otherwise consider an index map for O(1) removal.
src/rpc/misc.cpp (2)

1551-1559: Ordering semantics: ensure network-wide determinism.

startNumber assumes a stable, globally consistent order. With a vector this is insertion order; verify upstream insertion is deterministic across nodes, or sort by a canonical key (e.g., primitives::GetLTagHash). See suggestion on ConnectBlockSpark.


1590-1616: Unify with mobile ordering in tags+txids RPC.

getusedcoinstagstxhashes still uses GetSpends() (unordered_map) with startNumber filtering, which is non-deterministic. Use GetSpendsMobile() and guard with -mobile for consistency.

-    std::unordered_map<GroupElement, int, spark::CLTagHash>  tags;
+    std::vector<std::pair<GroupElement, int>>  tags;
     std::unordered_map<uint256, uint256> ltagTxhash;
     {
         LOCK(cs_main);
-        tags = sparkState->GetSpends();
+        if (!GetBoolArg("-mobile", false)) {
+            throw std::runtime_error(std::string("Please rerun Firo with -mobile "));
+        }
+        tags = sparkState->GetSpendsMobile();
         ltagTxhash = sparkState->GetSpendTxIds();
     }
-    int i = 0;
-    for ( auto it = tags.begin(); it != tags.end(); ++it, ++i) {
+    int i = 0;
+    for (auto it = tags.begin(); it != tags.end(); ++it, ++i) {
         if (cmp::less((tags.size() - i - 1), startNumber))
             continue;
         std::vector<unsigned char> serialized;
         serialized.resize(34);
-        it->first.serialize(serialized.data());
+        it->first.serialize(serialized.data());
         std::vector<UniValue> data;
         data.push_back(EncodeBase64(serialized.data(), 34));
         uint256 txid;
-        uint256 ltagHash = primitives::GetLTagHash(it->first);
+        uint256 ltagHash = primitives::GetLTagHash(it->first);
         if (ltagTxhash.count(ltagHash) > 0)
             txid = ltagTxhash[ltagHash];
         data.push_back(EncodeBase64(txid.begin(), txid.size()));
         UniValue entity(UniValue::VARR);
         entity.push_backV(data);
         serializedTagsTxIds.push_back(entity);
     }
src/spark/state.cpp (2)

1093-1101: Make insertion order deterministic across nodes.

mobileUsedLTags preserves insertion order, but AddSpend() is fed from pblock->sparkTxInfo->spentLTags, which is an unordered_map. Within a block with multiple spends, iteration order may differ by platform/build, breaking clients that rely on startNumber.

Sort the per-block spends in ConnectBlockSpark before calling AddSpend():

-            BOOST_FOREACH (auto& lTag, pblock->sparkTxInfo->spentLTags) {
-                pindexNew->spentLTags.insert(lTag);
-                sparkState.AddSpend(lTag.first, lTag.second);
-            }
+            // Ensure deterministic order for downstream mobile sync
+            std::vector<std::pair<GroupElement,int>> orderedSpends(
+                pblock->sparkTxInfo->spentLTags.begin(), pblock->sparkTxInfo->spentLTags.end());
+            std::sort(orderedSpends.begin(), orderedSpends.end(),
+                      [](auto const& a, auto const& b){
+                          return primitives::GetLTagHash(a.first) < primitives::GetLTagHash(b.first);
+                      });
+            for (auto const& lTag : orderedSpends) {
+                pindexNew->spentLTags.insert(lTag);
+                sparkState.AddSpend(lTag.first, lTag.second);
+            }

Add at file top if needed:

+#include <algorithm>

1108-1115: Prefer erase-remove to avoid iterator invalidation and remove duplicates if any.

Current loop erases the first match; use erase-remove for clarity and safety.

-    if (GetBoolArg("-mobile", false)) {
-        for (auto tag = mobileUsedLTags.begin(); tag != mobileUsedLTags.end(); tag++) {
-            if (tag->first == lTag) {
-                mobileUsedLTags.erase(tag);
-                break;
-            }
-        }
-    }
+    if (GetBoolArg("-mobile", false)) {
+        mobileUsedLTags.erase(
+            std::remove_if(mobileUsedLTags.begin(), mobileUsedLTags.end(),
+                           [&](const std::pair<GroupElement,int>& p){ return p.first == lTag; }),
+            mobileUsedLTags.end());
+    }

And include:

+#include <algorithm>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 055a67f and 784d071.

📒 Files selected for processing (3)
  • src/rpc/misc.cpp (1 hunks)
  • src/spark/state.cpp (4 hunks)
  • src/spark/state.h (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/spark/state.h (1)
src/libspark/spend_transaction.h (1)
  • vector (57-97)
src/spark/state.cpp (1)
src/spark/primitives.cpp (2)
  • tag (43-49)
  • tag (43-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build-guix-aarch64-linux-gnu
  • GitHub Check: build-guix-x86_64-apple-darwin
  • GitHub Check: build-windows-cmake
  • GitHub Check: build-guix-x86_64-w64-mingw32
  • GitHub Check: build-guix-arm64-apple-darwin
  • GitHub Check: build-guix-x86_64-linux-gnu
  • GitHub Check: Create Source Distribution
  • GitHub Check: build-mac-cmake
  • GitHub Check: build-linux-cmake
🔇 Additional comments (2)
src/spark/state.cpp (2)

959-960: LGTM: reset covers mobile vector too.


1494-1496: LGTM: accessor matches new storage.

Comment on lines +1545 to 1549
std::vector<std::pair<GroupElement, int>> tags;
{
LOCK(cs_main);
tags = sparkState->GetSpends();
tags = sparkState->GetSpendsMobile();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard RPC behind -mobile or provide a fallback.

tags now come from GetSpendsMobile(), which is only populated when -mobile=true. Without a guard, this RPC silently returns empty arrays on nodes started without -mobile. Align with other mobile RPCs and fail fast (or fallback to GetSpends()).

Suggested fix: fail if -mobile is not set.

 spark::CSparkState* sparkState =  spark::CSparkState::GetState();
+ if (!GetBoolArg("-mobile", false)) {
+     throw std::runtime_error(std::string("Please rerun Firo with -mobile "));
+ }
 std::vector<std::pair<GroupElement, int>>  tags;
 {
     LOCK(cs_main);
     tags = sparkState->GetSpendsMobile();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::vector<std::pair<GroupElement, int>> tags;
{
LOCK(cs_main);
tags = sparkState->GetSpends();
tags = sparkState->GetSpendsMobile();
}
// Acquire the Spark state
spark::CSparkState* sparkState = spark::CSparkState::GetState();
// Ensure mobile mode is enabled, otherwise fail fast
if (!GetBoolArg("-mobile", false)) {
throw std::runtime_error(std::string("Please rerun Firo with -mobile "));
}
std::vector<std::pair<GroupElement, int>> tags;
{
LOCK(cs_main);
tags = sparkState->GetSpendsMobile();
}

@reubenyap reubenyap requested a review from psolstice September 29, 2025 10:47

void CSparkState::RemoveSpend(const GroupElement& lTag) {
auto iter = usedLTags.find(lTag);
if (GetBoolArg("-mobile", false)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It makes sense to move the whole mobile part behind if (iter != usedLTags.end()) check to avoid long loop if there is no tag in our state

@reubenyap reubenyap merged commit 0df1855 into master Oct 14, 2025
29 of 30 checks passed
@reubenyap reubenyap deleted the getusedcoinstags branch October 14, 2025 12:06
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.

4 participants