Skip to content

server/market: Meshify market - #3630

Open
martonp wants to merge 72 commits into
decred:masterfrom
martonp:mesh-market
Open

martonp wants to merge 72 commits into
decred:masterfrom
martonp:mesh-market

Conversation

@martonp

@martonp martonp commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

This PR builds on #3629 and integrates server/market with mesh. It adds 40 commits (+16,745, -4,957).

It adds 5 commands:

  • limit: submit a limit order.
  • market: submit a market order.
  • cancel: submit a cancel order.
  • schedule_suspend: schedule a market suspension.
  • schedule_resume: schedule a market resumption.

It also adds 10 events:

  • order_accepted: records an accepted order in an epoch.
  • market_started: performs startup cleanup and sets the active epoch.
  • advance_epoch: closes the active epoch and opens the next one.
  • epoch_processed: runs matching and applies the epoch’s results.
  • orders_revoked: revokes orders.
  • market_suspend_scheduled: records the final trading epoch and whether to retain the book.
  • market_suspended: completes the suspension after the final trading epoch has been processed.
  • market_resume_scheduled: records the scheduled resumption epoch.
  • market_resumed: resumes trading, adopts trading parameters, and revokes retained orders that are no longer valid.
  • suspended_cancel: cancels a booked order while the market is suspended.

While the trading rules and general functionality of markets remain largely the same, mesh requires all state needed to apply events to be persisted and replicated. This lets the slave take over at any point and continue progressing the market. To achieve this, some things had to be redesigned:

  • Market lifecycle information and active trading parameters are now persisted in the market_lifecycle table. This includes epoch progress, scheduled suspensions and resumptions, and parameters such as lot size and rate step. Trading parameters still come from configuration, but become active through market_started and market_resumed events. When the two nodes connect, the mesh handshake ensures that their configuration files are compatible, but a slave may be reconnecting after a configuration change and may need to replay events that happened when the previous configuration was still active. Putting the trading parameters in the database allows for this.

  • Previously, NewMarket loaded state from the database and performed startup cleanup all in one step. Now, the in-memory representation of the persisted state is loaded from the database by LoadState, which is registered as a mesh state loader. The startup cleanup is handled by the market’s master worker emitting a market_started event, which revokes unfinished epoch orders and booked orders that are no longer valid.

  • Two events, advance_epoch and epoch_processed, drive epoch progression. Both are emitted by the market’s master worker. advance_epoch is emitted on a timer and closes the current epoch and opens the next one. epoch_processed is emitted after preimage collection finishes. It runs the matcher and then records the revealed preimages, new matches, and updated order states. To avoid processing falling behind epoch advancement, there is a requirement that there can only be two closed, unprocessed epochs at a time. Preimage request timeouts are set to min(2/3 * epoch duration, 20 seconds) to reduce delays.

  • All reputation effects from missed preimages and revoked orders are now handled by the event appliers for the epoch_processed and orders_revoked events directly, replacing direct calls made to auth.

  • Order submissions are now idempotent. A client can retry an identical request without creating a new order. If the previously accepted order is still active, the response contains the original result. If the original order was accepted within the last 30 minutes, but is now archived, an error is returned. This lets clients retry when an order was accepted but its response was lost, such as during failover.

  • The OrderFeed has been removed. The BookRouter still manages client book subscriptions, but event appliers update it directly instead of sending updates through the OrderFeed.

  • swapper.Negotiate was called by a market after it had found matches. It had three responsibilities: persisting matches, storing the matches in the swapper's memory, and requesting match acks from clients. Match persistence now happens during epoch_processed's DB applier, TrackMatches puts the matches into the swapper's in-memory state, and RequestMatchAcks, which is only called on the master, requests the match acks from the clients. TrackMatches and RequestMatchAcks are both left as stubs and will be implemented in the upcoming swap PR.

  • market.SwapDone is called by the swapper after a swap negotiation completes or fails. It used to update storage and reputation as well as update the market's in-memory state. Now it just updates the market's in-memory state, and the storage updates it used to be responsible for will be part of the swap_redemption_recorded and match_failed events, which will also be added in the upcoming swap PR.

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.
Adds `submitMarketStarted` to construct and submit a `market_started` event
containing the startup epoch, trading parameters, and orders to revoke.

It selects all unfinished epoch orders for revocation, along with booked
orders that are no longer valid because of incompatible lot sizes, spent
funding coins, or insufficient account balances.
Previously, changing a market's configured lot size caused `NewArchiver`
to flush its entire book during database initialization. Removes this
automatic flush so order revocations happen through `market_started`
events.
Adds `db.ApplyOrderAcceptedEvent` to store accepted orders with epoch status.
Defines the `order_accepted` event payload in `server/meshevents`.

Updates `OrderWithCommit` to check only active orders, so commitment validation
does not depend on archived history that may be absent after loading a snapshot.
Adds the `order_accepted` event applier to record accepted orders, update market
state, and notify order-book subscribers.

Updates `validateOrder` and the parcel calculations to use the market’s active
trading parameters. Updates `CheckParcelLimit` to use `UserReputationAt` with
the order’s server time, so a bond expiring before replication or replay does
not change the order’s parcel limit.
Adds `limit`, `market`, and `cancel` mesh commands that validate order requests
and emit `order_accepted` events. Updates the order router to submit requests
through mesh, replacing `SubmitOrder` and `SubmitOrderAsync` with
`AcceptOrderCommand`.

Adds `VerifyUserSig` to verify signatures without requiring a local client
connection. It loads the account’s public key from the database when no local
connection exists.
Adds idempotent handling of resubmitted order requests, allowing clients to
retry when they did not receive a response without creating another order.
`HandleOrderResubmission` returns the original order ID and server time for
recognized active orders, or an error if the order is already archived.

Adds `OrdersWithCommit` to find active orders and archived orders accepted
within the last 30 minutes, covering the client's retry period. This allows
resubmissions to be recognized even after an order is archived. Snapshots retain
30 days of archived order history, so restoring a snapshot preserves the history
needed for these checks.
Adds the `advance_epoch` event to describe closing the current epoch and
opening the next, or closing the final epoch before a scheduled suspension.
Adds `db.ApplyAdvanceEpochEvent` to update the stored market lifecycle,
advancing the active epoch or moving the market into the draining state.

Prevents the market from closing another epoch when two closed epochs are
still waiting to be processed.
Adds the `advance_epoch` event applier to close the current epoch and open
the next. When the final epoch of a scheduled suspension closes, it stops
order intake and waits for the remaining closed epochs to be processed.
Adds `applyRepEventTx`, which extends `applyEventTx` for event appliers that
record reputation outcomes. Appliers collect the outcomes alongside their other
database changes, and the helper records everything in one transaction.

The helper handles writing outcomes to the `points` table, pruning older
outcomes to the configured limits, and notifying the reputation listener when
cached reputation may need invalidation. This gives event appliers a common way
to update reputation without managing the points table themselves.
Adds `ApplyEpochProcessedEvent` to persist the results of collecting preimages
and running the matcher for an epoch. It stores revealed preimages, revokes
orders with missing preimages, and records order status and fill changes,
matches, and epoch reports. It also advances the market’s last processed epoch
and records reputation outcomes for preimage collection and cancellations, all
in one transaction.
Defines `EpochProcessedEvent` to carry an epoch’s preimage collection results
and matching inputs.

Adds `buildEpochProcessedUpdate` to run the matcher and prepare the order
updates, matches, and epoch report for storage. Matching uses copies of the book
and orders so a failed database update cannot leave live market state partially
changed. Also makes the ordering of matcher results deterministic.
Adds the `epoch_processed` event applier. It uses `buildEpochProcessedUpdate` to
prepare and persist the matching results before updating market state. After
storage succeeds, matching runs again on the live book, checking that its
matches agree with those stored.

Updates quantities awaiting settlement, funding locks, and the processed epoch.
Updates book subscriptions and market statistics, publishes match proofs and
epoch reports, and notifies users of unmatched orders and orders revoked for
missing preimages.

Replaces the market’s `Negotiate` call with separate `TrackMatches` and
`RequestMatchAcks` calls, separating swap tracking from requesting client
acknowledgements. These are initially stubs until the swap package is converted.
Previously, `processReadyEpoch` ran matching, stored the results, updated market
state and reputation, sent notifications, and initiated swap negotiation. It now
publishes an `epoch_processed` event containing the preimage collection results
and matching inputs, then requests client acknowledgements for the resulting
matches after the event is applied.

Preimage collection now only gathers results. Storing preimages, revoking missed
orders, and recording reputation outcomes happen when the event is applied.
Changes the preimage response timeout from a fixed 20 seconds to two-thirds of
the epoch duration, capped at 20 seconds. This reduces delays from unresponsive
clients now that epoch advancement is limited to two closed epochs awaiting
processing.

Adds a five-second minimum epoch duration so shorter configurations cannot make
the preimage response window too small.
The swapper calls `Market.SwapDone` when a match finishes processing,
successfully or unsuccessfully, to update the market’s state for each order
involved. Updates `SwapDone` to reduce quantities awaiting settlement or, when
an order is at fault, remove its booked remainder, release its funding locks,
and notify its owner. When it removes an order from the market’s book, it
returns that order to the caller so the `dex` package can tell `BookRouter` to
remove it from its cached book and notify book subscribers.

Removes database and reputation updates from `SwapDone`, preparing for those
changes to be handled by swap event appliers. `SwapDone` now only updates
in-memory market state and sends the associated notifications.
Defines `OrdersRevokedEvent` for server-initiated revocations, targeting either
specific orders on a market or all of a user’s booked orders across markets.
Supports revocation for spent funding, prolonged disconnection, insufficient
trading tier, or an operator request.

Adds `ApplyOrdersRevokedEvent` to revoke the selected booked orders, create
their corresponding server-generated cancel orders, and record reputation
outcomes without cancellation penalties, all in one transaction.
Adds the `orders_revoked` event applier. It finds the booked orders identified
by the event and revokes them with `ApplyOrdersRevokedEvent` before removing
them from the market’s book and settlement tracking, releasing their funding
locks, and notifying their owners and book subscribers.

When the event specifies insufficient trading tier as the revocation reason, it
also sends the affected users a penalty notification.
Previously, `CheckUnfilled` directly unbooked a user’s unfilled orders when
their funding coins were found spent. It now publishes an `orders_revoked` event
and returns the revoked orders after the event is applied, leaving storage
updates, funding unlocks, and notifications to the event applier.
Adds the `schedule_suspend` command, which selects the final trading epoch from
the requested suspension time and emits a `market_suspend_scheduled` event.

Applying the event records the suspension schedule and whether to retain the
book. The market continues trading through its final epoch.
Adds the `market_suspended` event to complete a scheduled suspension after the
final epoch has been processed. Applying the event marks the market suspended
and either retains the book or revokes its remaining orders, releasing their
funding locks and notifying clients.
Adds the `schedule_resume` command, which selects the first epoch starting after
both the requested time and the current time, and emits a
`market_resume_scheduled` event.

Applying the event records the resumption schedule. The market remains suspended
until a `market_resumed` event is applied.
Adds `submitMarketResume` to check retained orders before resuming trading and
emit a `market_resumed` event. The event contains the configured trading
parameters and orders to revoke because of incompatible lot sizes, spent funding
coins, or insufficient account balances.

Applying the event revokes those orders, adopts the trading parameters, and
returns the market to the running state. Affected order owners and book
subscribers are notified.
Updates `SuspendMarket` and `ResumeMarket` in the `dex` package to submit
`schedule_suspend` and `schedule_resume` commands through mesh instead of directly
scheduling suspension or restarting the market subsystem.

Previously, `SuspendMarket` and `ResumeMarket` updated the configuration served
to clients and broadcast notifications directly. Now those updates happen
through a callback after the corresponding event is applied. The commands and
callback are connected to the mesh service in the later market worker
integration commit.
Defines `SuspendedCancelEvent` for canceling a booked order while its market is
suspended, without waiting for epoch processing.

Adds `ApplySuspendedCancelEvent` to record the executed cancel order, mark its
target canceled, and store the cancellation match in one transaction.
Updates `AcceptOrderCommand` to emit a `suspended_cancel` event when canceling a
booked order on a suspended market.

Applying the event persists the cancellation, removes the order from the book,
releases its funding locks, and notifies clients.
Adds `marketEpochDriver` to run the market using the previously introduced
events. It performs startup cleanup, advances epochs at their scheduled
boundaries, collects preimages, and publishes `epoch_processed` events once
collection finishes. It also completes suspensions after the final epoch is
processed and resumes markets when their scheduled time arrives.

The driver waits for asset backends to synchronize before starting or resuming
trading. The driver is connected to `Market.Run` and registered as a mesh master
worker in a later commit.
Previously, `AddMarketSource` loaded candle history as each market was
registered. It now only registers the market, and `LoadCaches` loads the history
separately. This allows candle caches to be initialized after market state is
restored, using the stored epoch duration.
A `market_started` event can change the epoch duration after candle caches have
been loaded. Updates `ReportEpoch` to adjust the epoch candle cache when this
happens, preserving the standard candle caches and their history.

Ensures standard candles are persisted even when their interval matches the
epoch duration. Also fixes the history cutoff in `LoadEpochStats` so candle
restoration includes the epoch ending exactly at that cutoff.
Connects the market package to the mesh service by registering its order and
lifecycle commands, event appliers, state loaders, and lifecycle callback.
Replaces the legacy `Market.Run` loop with `marketEpochDriver` and registers
markets as mesh master workers.
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