// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "transaction_history_store_policy.h" #include namespace dragonx { namespace data { namespace { constexpr std::uint64_t kOffset = 0xcbf29ce484222325ULL; constexpr std::uint64_t kMul = 0x9E3779B97F4A7C15ULL; // odd static_assert(sizeof(double) == sizeof(std::uint64_t), "amount bit pattern is hashed as 64 bits"); // 8 bytes per step: a 5k-row wallet (~1.5 MB of row text) hashes in about a millisecond. Endianness-dependent // on purpose: the value never leaves the process. inline void mixBytes(std::uint64_t& h, const void* data, std::size_t n) { const auto* b = static_cast(data); while (n >= 8) { std::uint64_t w; std::memcpy(&w, b, 8); h = (h ^ w) * kMul; h ^= h >> 32; b += 8; n -= 8; } if (n) { std::uint64_t w = 0; std::memcpy(&w, b, n); h = (h ^ w ^ (static_cast(n) << 56)) * kMul; h ^= h >> 32; } } inline void mixU64(std::uint64_t& h, std::uint64_t v) { mixBytes(h, &v, sizeof(v)); } inline void mixStr(std::uint64_t& h, const std::string& s) { mixU64(h, s.size()); // length first: ("ab","c") != ("a","bc") mixBytes(h, s.data(), s.size()); } inline std::uint64_t finalize(std::uint64_t x) // splitmix64 finalizer { x += 0x9e3779b97f4a7c15ULL; x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; return x ^ (x >> 31); } // The thresholds a stored row is read at: transactions_tab.cpp:788-793 / transaction_details_dialog.cpp:80-85 // (0 pending, < 10 counted, >= 100 mature for mined), mining_earnings.cpp:102 (>= 100), the confirmed-tx cache // (>= 10, app_network.cpp:1915) and conflicted (< 0, network_refresh_service.cpp:1470). inline std::uint64_t confirmationBucket(int c) { return c < 0 ? 0 : c == 0 ? 1 : c < 10 ? 2 : c < 100 ? 3 : 4; } } // namespace HistorySnapshotSig historySnapshotSignature(const std::vector& transactions, const std::unordered_set& skipTxids, const std::string& walletHash, std::uint64_t keyId, const std::unordered_map& shieldedScanHeights, int tipHeight, const std::string& tipHash, int sentHeight, int sentAnchorHeight, const std::string& sentAnchorHash, const std::unordered_set& sentBacklog) { HistorySnapshotSig sig; std::uint64_t rowSum = 0; // sums: commutative, so row order does not matter; duplicates still count std::uint64_t exactSum = 0; for (const auto& tx : transactions) { if (!skipTxids.empty() && skipTxids.count(tx.txid)) continue; std::uint64_t h = kOffset; mixStr(h, tx.txid); mixStr(h, tx.type); std::uint64_t amountBits = 0; std::memcpy(&amountBits, &tx.amount, sizeof(amountBits)); mixU64(h, amountBits); mixU64(h, static_cast(tx.timestamp)); mixStr(h, tx.address); mixStr(h, tx.from_address); mixStr(h, tx.memo); std::uint64_t e = h; mixU64(h, confirmationBucket(tx.confirmations)); mixU64(e, static_cast(static_cast(tx.confirmations))); rowSum += finalize(h); exactSum += finalize(e); ++sig.rows; } std::uint64_t backlogSum = 0; // order-independent: a set, so sum is commutative for (const auto& txid : sentBacklog) { if (txid.empty()) continue; std::uint64_t h = kOffset; mixStr(h, txid); backlogSum += finalize(h); } std::uint64_t c = kOffset; mixU64(c, rowSum); mixU64(c, sig.rows); mixStr(c, walletHash); mixU64(c, keyId); mixU64(c, backlogSum); sig.content = finalize(c); std::uint64_t heightSum = 0; for (const auto& [address, height] : shieldedScanHeights) { if (address.empty() || height < 0) continue; // exactly what replace() persists std::uint64_t h = kOffset; mixStr(h, address); mixU64(h, static_cast(height)); heightSum += finalize(h); } std::uint64_t s = kOffset; mixU64(s, heightSum); mixU64(s, exactSum); mixU64(s, static_cast(static_cast(tipHeight))); mixStr(s, tipHash); mixU64(s, static_cast(static_cast(sentHeight))); mixU64(s, static_cast(static_cast(sentAnchorHeight))); mixStr(s, sentAnchorHash); sig.slow = finalize(s); return sig; } HistoryStoreAction decideHistoryStore(const HistoryStoreInput& in) { const bool contentDirty = !in.haveStored || in.stored.content != in.current.content; const bool slowDirty = in.haveStored && in.stored.slow != in.current.slow; if (in.mode == HistoryStoreMode::Now) return (contentDirty || slowDirty) ? HistoryStoreAction::Store : HistoryStoreAction::Skip; if (contentDirty) return in.sinceLastSubmitSec >= kHistoryMinStoreIntervalSec ? HistoryStoreAction::Store : HistoryStoreAction::Defer; if (slowDirty && in.sinceLastSubmitSec >= kHistorySlowStoreIntervalSec) return HistoryStoreAction::Store; return HistoryStoreAction::Skip; } } // namespace data } // namespace dragonx