Value data fragments carry no node ID. Parts of a message received in
envelopes must all come under the sender key of the envelopes, so that
plaintext or foreign-key parts spoofed from the sender's address, which
would otherwise be merged first, are ignored.
Replies to Initial and Hello envelopes must not cache a session at the
source address, which can be spoofed, and the cache must evict the least
recently used session rather than the lowest address. A node that started
with an Initial envelope only switches to Data envelopes once the peer
answered a Session envelope, which is when the peer caches the session.
Steady-state traffic uses Data envelopes, without sender keys nor credential, once the peer answered a request. Data envelopes only open from the session address, also authorize writes, and survive the receiver's epoch rotation.
Encrypted writes must come in a Session envelope, whose ephemeral key is bound to the sender address. Plaintext writes still need a token, now issued by the network engine.
The previous ephemeral key is accepted for one more period, then erased.
A request retry restarts with an Initial envelope and our credential.
A hello is only answered and doesn't change what is known of its sender.
Query::isSatisfiedBy() considers an unfiltered answer sufficient for a
filtered query, which is true for a vanilla get: onGetValuesDone() applies
the operation filter to the values before calling back.
A projected query is different. It receives the fields another query selected
and can no longer evaluate its own filters on them, all the more so as those
filters may target fields that were not selected at all. Two concurrent
queries such as "SELECT id" and "SELECT id WHERE value_type=1" therefore made
the filtered callback report identifiers whose type does not match, and
searchNodeGetDone() additionally cancelled its own request so it never got a
correctly filtered answer.
Add Query::isProjectionSatisfiedBy(), which additionally requires both queries
to apply the same filters, and use it for operations with a query callback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
containedIn() only checked that the other index declared the same field
keys, never that they held the same values, contradicting its documented
contract of containment "with the same value".
Dht::query() uses containedIn() to deduplicate projected results. Because
every result of a given query shares the same projection schema, any two
results compared equal: the first one suppressed all the later ones, and the
"lesser" branch even erased the already reported result. A query selecting a
field therefore reported a single value regardless of how many matched.
Compare the FieldValue as well, and add a regression covering distinct
values, equal indexes and a narrower projection contained in a wider one.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the fake push gateway of the full chain test into a reusable
helper, and add two tests showing that the proxy server pushes to the
wrong platform:
- when a device refreshes its subscription with another platform,
new values keep being pushed with the platform of the first
subscription;
- a platform set after the proxy client started is not used by its
subscriptions.
Co-authored-by: Copilot (Claude Opus 5.5) <223556219+Copilot@users.noreply.github.com>
Prefix::size_ counts bits, but commonBits walked content_ a byte at a
time up to it. Two prefixes that agree on their whole common length are
therefore read well past their end, which AddressSanitizer reports as a
heap-buffer-overflow, and the count that comes back is eight times too
large. A lookup with a common prefix length hits this as soon as an
index entry matches the key it is looking for, and entry.prefix comes
straight off the network. The flags were indexed the same way, so a
partially known prefix was read at the wrong position too.
swapBit had the other half of the confusion: it flipped bit
(8 - bit) % 8 of the byte, while isActiveBit reads bit 7 - (bit % 8).
getSibling, which flips the last bit of a prefix to name the sibling
node in the trie, therefore returned a prefix differing in some other
bit and sharing the last one, so lookup and insertion looked up the
wrong node. Splitting a leaf and finding a canary both rely on it.
Add tests covering both, which fail before the fix: commonBits returns
193 for two identical 160 bit prefixes, and a prefix and its sibling
share 7 bits rather than all but the last.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Require immediate completion before run and after join, including a burst of notifications. Keep active-runner coverage.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Value::operator== ignores seq, so a node re-announcing identical content
with a bumped seq is the same value refreshed, not an edition. ValueCache
must not re-emit it: a source-counting OpValueCache above would count the
node twice and keep the value after the node is gone.
A source reporting a different payload with the same seq is ignored on
add and not counted; its later expiration must not release the value
still held by the counted sources.
OpValueCache counted every re-delivery of an identical value as a new
source, but the proxy client is a single logical source that re-sends
values on push refresh or listen restart: the refcount grew unbounded
and expirations were never propagated. Add a countSources flag to
OpValueCache, OpCache and SearchCache; the proxy client disables it.
When a value is edited (same id, higher seq), only the reporting source
is known to hold the new version: replace it in place, reset the
refcount to 1 and emit a single add. Ignore older versions from lagging
sources on add, and ignore their expiration so they cannot release a
newer version still held by others.
ValueCache emits an edited value as an add instead of silently
refreshing its old copy, and applies a grace period of one type
expiration while the listen is synced so that a single lost refresh
notification does not drop a value the remote node still holds.
SearchCache::get only answers from the cache once the listen is synced,
so a get never returns a partial view that the network would complete.
Add the single-source OpCacheTester cases enabled by countSources.
OpCacheTester and ValueCacheTester exercise the listen caches in
isolation: add/edit/expire flows, per-source refcounting, stale
versions, sync status, SearchCache::get and node drop.
DhtRunnerTester and DhtProxyTester gain end-to-end listen tests for
value edition, duplicate puts, multi-source and partial expiration,
get after a synced listen, and the push refresh/expire paths.
The proxy fixture joins the proxy DhtRunner before destroying the
DhtProxyServer so no DHT callback can reach a destroyed server, and
push notifications are never awaited while holding the mutex taken by
listen callbacks: the result is produced on the DHT thread.
Register the new executables in CMake and Meson.