perf(chat): sends re-read by origin, chat scans under the scan budget
A z_sendmany submit set off a burst of wallet scans before anything had changed. Ops now re-read the wallet when they finish, by who started them: the user's sends and dialog ops get a re-read plus one forced balance once none of their ops is pending; client auto-shield re-reads unforced; chat messages and splits refresh only the chat note view. A pending send no longer forces scans, and chat txids no longer enter the tracked-send set. Chat ingest skips stored messages before decrypting and caches undecryptable ones per identity. The inbox scan runs when getwalletinfo's txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety net is due; the note-buffer scan runs on need. Both are admitted by the scan budget, on a steady clock. The chat payer comes from a per-address view of verified notes, with debits re-applied across in-flight scans; splits are sized to one payer and only when they can refill the buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,13 @@ ChatService::~ChatService() {
|
||||
void ChatService::setIdentity(const ChatKeyPair& keys) {
|
||||
identity_ = keys;
|
||||
has_identity_ = true;
|
||||
undecryptable_.clear(); // another identity may decrypt them
|
||||
}
|
||||
|
||||
void ChatService::clearIdentity() {
|
||||
wipeChatKeyPair(identity_);
|
||||
has_identity_ = false;
|
||||
undecryptable_.clear();
|
||||
}
|
||||
|
||||
int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata,
|
||||
@@ -37,6 +39,9 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
|
||||
int added = 0;
|
||||
for (const auto& meta : metadata) {
|
||||
// Already stored: append would refuse it. Every scan re-sends the whole history, so this skips the
|
||||
// decrypt for all but the new messages.
|
||||
if (store_.contains(meta.txid, meta.payload_position)) continue;
|
||||
// A memo whose sender is our OWN identity is something we sent (only we hold our key). The local
|
||||
// echo already records it as outgoing — ingesting it as incoming would duplicate it as a phantom
|
||||
// "from peer" message. (This also collapses same-seed self-chat, where the "peer" wallet shares
|
||||
@@ -78,11 +83,23 @@ int ChatService::ingest(const std::vector<HushChatTransactionMetadata>& metadata
|
||||
message.body = meta.payload_memo; // plaintext request text
|
||||
} else {
|
||||
message.kind = ChatMessageKind::Message;
|
||||
// Keyed by the decrypt's inputs, not only (txid, position): another scan path that pairs a
|
||||
// different header with this payload gets its own try.
|
||||
const std::string failKey = ChatStore::messageKey(meta.txid, meta.payload_position) + "|" +
|
||||
meta.sender_public_key_hex + "|" + meta.secretstream_header_hex;
|
||||
if (undecryptable_.count(failKey)) continue;
|
||||
++decrypt_attempts_;
|
||||
std::string plaintext;
|
||||
const ChatCryptoStatus status = decryptIncoming(
|
||||
identity_, meta.sender_public_key_hex, meta.secretstream_header_hex,
|
||||
meta.payload_memo, plaintext);
|
||||
if (status != ChatCryptoStatus::Ok) continue; // drop undecryptable silently
|
||||
if (status != ChatCryptoStatus::Ok) { // drop undecryptable silently, and don't try it again
|
||||
if (status != ChatCryptoStatus::SodiumInitFailed) { // not a property of the message
|
||||
if (undecryptable_.size() >= 4096) undecryptable_.clear();
|
||||
undecryptable_.insert(failKey);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
message.body = std::move(plaintext);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx::chat {
|
||||
@@ -99,9 +100,15 @@ public:
|
||||
const ChatStore& store() const { return store_; }
|
||||
ChatStore& store() { return store_; }
|
||||
|
||||
std::uint64_t decryptAttempts() const { return decrypt_attempts_; } // tests / diagnostics
|
||||
|
||||
private:
|
||||
ChatKeyPair identity_{};
|
||||
bool has_identity_ = false;
|
||||
// Decrypt failures, keyed by what the decrypt reads (bounded). A rescan re-sends every message ever
|
||||
// received, so without it each poll re-ran the crypto for every undecryptable memo.
|
||||
std::unordered_set<std::string> undecryptable_;
|
||||
std::uint64_t decrypt_attempts_ = 0;
|
||||
ChatStore store_;
|
||||
ChatDatabase* db_ = nullptr; // optional; not owned
|
||||
std::function<bool(const std::string&)> blocked_pred_; // true => cid is blocked (drop its messages)
|
||||
|
||||
@@ -6,8 +6,12 @@
|
||||
|
||||
namespace dragonx::chat {
|
||||
|
||||
std::string ChatStore::messageKey(const std::string& txid, std::size_t payloadPosition) {
|
||||
return txid + ":" + std::to_string(payloadPosition);
|
||||
}
|
||||
|
||||
std::string ChatStore::dedupKey(const ChatMessage& message) {
|
||||
return message.txid + ":" + std::to_string(message.payload_position);
|
||||
return messageKey(message.txid, message.payload_position);
|
||||
}
|
||||
|
||||
bool ChatStore::append(const ChatMessage& message) {
|
||||
|
||||
@@ -20,6 +20,13 @@ public:
|
||||
// Returns true if newly inserted, false if a duplicate was ignored.
|
||||
bool append(const ChatMessage& message);
|
||||
|
||||
// The dedup key of a message: "<txid>:<payload_position>".
|
||||
static std::string messageKey(const std::string& txid, std::size_t payloadPosition);
|
||||
// A message with this (txid, position) is in the store (append would refuse it).
|
||||
bool contains(const std::string& txid, std::size_t payloadPosition) const {
|
||||
return seen_.count(messageKey(txid, payloadPosition)) != 0;
|
||||
}
|
||||
|
||||
// Messages in a conversation, in insertion order.
|
||||
std::vector<ChatMessage> conversation(const std::string& conversationId) const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user