Skip to content

client: Client updates for mesh - #3633

Open
martonp wants to merge 119 commits into
decred:masterfrom
martonp:mesh-client
Open

martonp wants to merge 119 commits into
decred:masterfrom
martonp:mesh-client

Conversation

@martonp

@martonp martonp commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

This PR builds on #3632 and updates the client to work with the mesh. It adds 13 commits (+2,946, −422).

  • Makes the client mesh-aware. It reads the mesh endpoints from the updated config response and handles mesh_endpoints notifications that update the set of endpoints. When its connection to one node fails, it automatically attempts to connect to the other node.

  • Shows mesh endpoints and the active connection in DEX settings, and notifies the user when the connection switches endpoints.

  • Prevents adding the same mesh twice by recognizing endpoints that share the same DEX public key.

  • To improve the user experience during failover, the client automatically retries order, cancellation, bond, and settlement requests after timeouts, disconnections, or temporary server errors.

  • Since the server no longer sends bondexpired notifications, the client now uses its own periodic bond checks to subtract the strength of expired bonds from its bonded tier and notify the user. It uses the BondExpiryThreshold returned by the server to avoid subtracting the same bond’s contribution twice.

  • Fixes several client bugs involving repeated match requests, reconnect handling, concurrent account updates, and cancellation requests.

Create the event log table to store each event's sequence number, kind,
payload, transaction data, and tip hash.

Add `applyEventTx` to run an event's database changes and append its log
entry in one transaction. Both are committed together, and a mismatch
with a supplied sequence number or expected tip hash rolls back the
changes. This helper is for use by event appliers.

Add `EventLogFrontier` and `EventLogEntriesAfter` so the mesh package can
query the event log table.
Define which tables are included in snapshots, which must be empty
before loading a snapshot, and which are preserved as local configuration.
Check at startup that every table in `public` and the stored market schemas
has a classification to avoid future developers adding tables without
considering how snapshotting will be affected.

Add `HasNoEventSourcedState` to check whether all event-sourced data is
absent, as required before loading a snapshot. Add `WipeEventSourcedState`
to clear all event-sourced data before restoring from a peer.
Add `WriteSnapshot`, used by the master to create a snapshot that is
sent to the slave, and also add `LoadSnapshot`, used by the slave to
populate its DB with the master's data.

The snapshot includes:

- The event-log sequence number and tip hash corresponding to the snapshot.
- All accounts, bonds, prepaid bonds, and reputation points.

For configured markets, it also includes:

- Market lifecycle state.
- Active orders, cancels, and matches.
- The past 30 days of archived orders, archived cancels, and matches, plus
  older archived orders referenced by included matches or archived cancels.
- The latest 1,000 candles from each candle table.
- Recent epoch reports needed to rebuild unfinished candles.
Add the v9 upgrade and update table schemas. The upgrade makes the
following changes:

- Create the `event_log` table.
- Create the `market_lifecycle` table to store market state, scheduled
  suspensions and resumptions, epoch progress, and trading parameters.
- Remove the legacy `accounts.fee_asset` column.
- Remove commitment and preimage uniqueness constraints from
  `orders_archived` and `cancels_archived`. Nodes may retain different
  archived histories, so these constraints could make archiving the same
  order succeed on one node and fail on another.
- Add non-unique commitment indexes to `orders_archived` and
  `cancels_archived` for lookups.
- Add indexes on `makerOrder` and `takerOrder` for active matches, to
  support finding an order's active matches during future swap processing.
- Add a genesis event when a database has existing event-sourced data and
  an empty event log. It includes a random nonce, so each legacy database
  initialized this way has a distinct history. Databases with no
  event-sourced data do not receive a genesis event.
Return account lookup errors separately from an unknown account, and update auth
callers to handle them. This prevents database failures from being treated as
missing accounts or empty bond lists when connecting, posting bonds, or
calculating reputation. This is needed for mesh because a failed database lookup
must not be cached as though the account does not exist.

Pass the caller's context through account and bond queries so cancellation and
deadlines cover the entire lookup.
Introduce the `server/meshevents` package as the catalog of mesh events shared
by the database and application packages. Define events for bond posting,
prepaid bond creation, and reputation forgiveness. Add JSON decoding for order
and match IDs to complement their existing JSON encoding.
Adds `repCache`, an LRU cache for account scores and bonds, to reduce repeated
database reads.

Previously, reputation calculations used active bonds and recent match,
preimage, and order outcomes held in memory for connected users. With mesh, both
nodes need to calculate reputation regardless of which node a user is connected
to.

The cache accepts a caller-supplied function that loads calculated scores and
bond data. Integration with database reads and reputation lookups is added in
subsequent commits.
Previously, legacy reputation was converted to `points` when an account's
reputation was first loaded. With mesh, snapshots can omit older order and match
history needed for that conversion, so converting lazily could produce different
scores on different nodes.

Converts all remaining version-0 accounts during the v9 database upgrade
instead, before mesh starts. This completes the conversion once, so reputation
lookups can read directly from `points` and snapshots include the converted
data.
Previously, reputation lookups used bonds and recent outcomes held in memory for
locally connected users. Updates these lookups to calculate reputation from
stored outcomes and bonds, using `repCache` to cache calculated scores and bond
data. This allows reputation to be calculated independently of which mesh node
the user connects to, without active users repeatedly querying the database.

Adds `UserReputationAt` to calculate reputation with bond expiry evaluated at a
supplied time. Stops periodic bond-expiry checks and retains expired bonds so
their contribution can still be calculated when replaying earlier events.

Adds `BondExpiryThreshold` to `account.Reputation` so clients can determine
which bonds were included in the reported tier. With periodic bond-expiry
notifications disabled, clients will need to track expiry themselves.

Adds `AcctRepStatus` with an error return so callers can distinguish a failed
reputation lookup from an unknown account or a zero tier. This will be required
by the unbooking logic to avoid removing orders when reputation cannot be
retrieved.
Adds `SetReputationInputsListener` and registers an auth callback that
invalidates affected cache entries when notified. The callback refreshes
reputation and sends score-change notifications to locally connected user.

The event appliers added in subsequent commits will invoke the listener after
updating reputation inputs.
Replaces the functions called by the market and swap packages to record
reputation outcomes with stubs. These stubs will be removed when those packages
are updated to record outcomes through mesh events.
Adds support for delivering requests and notifications to clients connected
through a mesh peer, routing responses back to their handlers, and expiring
unanswered requests. Keeps local-only delivery available for node-local
notifications.
Adds `ApplyBondPostedEvent` to store posted bonds, create accounts when needed,
and consume prepaid bond tokens. Returns the account's bonds and latest
reputation outcomes from the same transaction so auth can calculate the
reputation returned with the bond response.
Adds the `postbond` command, which validates on-chain bonds and prepaid-token
redemptions, waits for any missing confirmations, and emits a `bond_posted`
event for newly accepted bonds. Adds the `bond_posted` event handler, which
stores the account and bond and consumes the prepaid token when redeeming one.

Adds the initial mesh setup in the `dex` package. Since dcrdex configuration
options for mesh have not been added yet, the service runs in single-server
mode.
Adds `ApplyPrepaidBondsCreatedEvent` to store newly created prepaid bond tokens.
Adds the `create_prepaid_bonds` command, which generates prepaid bond tokens
with the requested strength and lifetime and emits a `prepaid_bonds_created`
event. Adds the event handler, which stores the tokens in the db.
Adds `ApplyReputationForgivenEvent` to forgive all penalties on an account or
the penalties associated with a single match.
martonp added 30 commits October 8, 2026 03:11
Resends unanswered `match`, `audit`, and `redemption` requests periodically so
settlement can continue after requests or responses are lost, including during
failover. Requests are rebuilt from the recorded match state and sent by the
master to users connected to either node. Adds periodic resending of
counterparty address notifications until the recipient’s swap contract is
recorded; previously, these notifications were sent when both addresses became
available and when a user reconnected.

Updates `UserConnected` to resend the user’s pending requests immediately when
they connect to the master, avoiding the wait for a periodic retry.
Fixes swap coin locker assignment so UTXO funding coins remain locked during
settlement.
…se windows

Adds `resumePendingRequests` to resend unacknowledged match, audit, and
redemption requests, and `extendInactionDeadlines` to give clients a fresh
response window.
- Runs the swapper as a mesh master worker before the market workers so it
  can handle matches as markets start.
- Adds a final master worker that enables inactivity checks after all market
  workers report readiness, giving clients a fresh response window so startup
  delays do not cause inactivity penalties.
- Moves active swap restoration from `NewSwapper` to a mesh state loader.
- Resends pending requests when the swapper starts.
Ignore net.ErrClosed when updating a read deadline after the connection has
closed.
Adds configuration for the peer's mesh address, the local mesh listener, the
client address advertised for failover, and the peer's TLS certificate.
Builds the compatibility snapshot from the network, asset, market, and trading
settings that mesh peers must agree on.
Tracks when DEX shutdown has begun so a mesh halt during shutdown does not
request another shutdown.
Uses the mesh configuration, when provided, to connect the mesh service to
another DEX node instead of running in single-server mode.
Publishes client-facing mesh endpoints in the configuration response and
notifies connected clients when the advertised endpoints change.
Adds `--meshforkreset` as an operator recovery tool for a node whose event
history has diverged from its peer. It requires the confirmation token from
the fork error, then clears the event log and event-sourced state so the
node can reload state from its peer.
Previously, `AuthManager` started a timer when a user disconnected and revoked
their orders if they did not return in time. It also revoked orders when
reputation penalties lowered their trading tier below 1. With mesh,
disconnecting from one node does not mean the user is disconnected from the
other.

Adds a master worker in `server/dex` that periodically checks users with booked
orders. It checks connectivity on both nodes before revoking orders for
prolonged disconnection, and checks reputation before revoking orders for
insufficient tier with penalties. `orders_revoked` events are issued for these
revocations.
- Updates `Notify` to return delivery errors to the admin API instead of always
  reporting success.
- Updates `NotifyAll` to broadcast to clients on both nodes, logging an error if
  delivery to the peer cannot be confirmed.
Restricts individual match forgiveness to single-server mode because snapshots
may omit the historical match data needed to apply it consistently on both nodes.
Adds a `/mesh` admin endpoint and a status panel in dexadm showing the node's
mode, peer connection, replication progress, failover timing, and halt errors.
Removes unused auth callbacks and storage APIs replaced by event application,
along with obsolete queries, legacy account fee-key support, and unused test
stubs.
Expires pending requests immediately when the connection breaks, allowing
callers to retry without waiting for their response timers.

Also includes cleanup of connection handling:

- Uses the same function to clear pending requests on disconnection and
  shutdown. Runs their callbacks after releasing the mutex.
- Gives each read loop its websocket directly, removing the need to lock and
  read the shared websocket pointer.
- Avoids blocking when a reconnect is already queued and consolidates read-error
  handling.
- Checks that a websocket exists before sending and rechecks the connection
  status after acquiring the write mutex.
Adds `FailoverWsConn` for connecting to a mesh through a list of endpoints. It
initially connects to the first endpoint and tries the remaining endpoints in
round-robin order when reconnecting.

`SetFailoverEndpoints` updates the endpoint list without interrupting the
current connection, and `ActiveEndpoint` reports which endpoint is connected.
Adds `MeshEndpoints` to `AccountInfo` to save the failover endpoints advertised
by a DEX, including their hosts and certificates. This lets the client reconnect
to another mesh node after restarting.
Fixes a preexisting race where concurrent account updates could overwrite each
other’s changes. Saving mesh endpoints introduces another source of these
updates. Replaces `UpdateAccountInfo` with `UpdateAccount`, which atomically
updates the account info.
- Replaces `WsConn` with `FailoverWsConn` for DEX connections.
- Initializes connections using saved endpoints.
- Applies and saves endpoint updates received through server configuration and
  `mesh_endpoints` notifications.
Prevents adding the same mesh twice by checking whether an endpoint’s DEX public
key matches an already configured DEX. The check also applies when posting or
redeeming a bond to register an account.
Shows the registered host and advertised mesh endpoints in the DEX settings
page, indicating which endpoint is currently connected. Updates the list when
the server advertises new endpoints and notifies the user when the connection
switches between endpoints.
Fixes a reconnect bug where a match reported by the server could be marked
revoked if the client failed to process it, such as when a wallet could not
provide a swap address. Tracks these failures separately so they are not
mistaken for matches missing from the server’s response.
Ensures the client reuses the stored swap address for a given match, avoiding a
situation where a repeated match request causes the client to advertise a new
address while still expecting the original address in the counterparty’s swap
contract. It does this by saving new matches before acknowledging them and using
the stored address in every acknowledgement.
Previously, submitting a cancellation held the trade lock while waiting for the
server, blocking settlement and order queries. Releases the lock during
submission so slow responses and failover retries do not block that work.
Updates `signAndRequest` to retry the same signed request after timeouts,
broken connections, and temporary server errors, allowing requests to
recover during failover.
The mesh conversion removed the server's periodic bond-expiry checks and
`bondexpired` notifications. The client now updates its bonded tier and
notifies the user when bonds expire, without waiting for a server notification.

Bond maintenance subtracts the strength of newly expired confirmed bonds from
the reported tier. When receiving a reputation update, the client uses
`BondExpiryThreshold` to identify bonds the server has already excluded, so
their strength is not subtracted again.
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.

1 participant