fix(solver)!: midnight-intents-swaps conformance — lower exact-output + executable liquidity + compose E2E (stacked on #48) - #52
Merged
acedward merged 7 commits intoSep 1, 2026
Conversation
…cit surplus disposition
Remediates 00003 P4-F01 (CRITICAL) / 00005 FR-001, SC-001.
The relay only ever promises a taker AT MOST the interpolated output of a
published ladder and then dispatches the taker's OWN demand
(reference relay-ws.ts solverAcceptsPrice `output >= requiredOutput`,
router/jobId.ts sendSwap `quote.requiredOutput`). resolveSwapJobRoute
required `interpolateQuote(levels, amountIn) === amountOut` exactly, so
every reference-valid job with a lowered demand was refused
`route_not_current` and the solver could not settle against the reference
relay at all.
Admission is now `0 < amountOut <= interpolateQuote(levels, amountIn)`.
The maker prefix selection rule is unchanged (largest whole-offer prefix
with `input <= amountIn`); the signed difference to the job is split into
two non-negative fields on ResolvedRoute, never both positive:
residualOut tokenOut the solver PAYS from inventory (demand above the
prefix payout) — Stock payout + reservation + published
residualBound checks unchanged;
surplusOut tokenOut the maker prefix pays OVER the demand and the
solver RETAINS — inflow only, so no Stock reservation
(the reference solver keeps `dy - requiredOutput` the
same way).
The settlement half builds one solver balancing leg: it spends
`residualOut` tokenOut (or nothing at all) and creates outputs to the
solver's own shielded address for the retained `residualIn` tokenIn
and/or `surplusOut` tokenOut. An outputs-only half is a legitimate
unbalanced zswap half; the maker offers balance it on merge. The leg
keeps journal kind RESIDUAL_BUILD and label `residual` in both
directions because restoreRecoveryTargets pairs the unproven leg with
its finalized contribution by that key suffix.
assertInverseHalf is deliberately unchanged: retained value is an output
to the solver's own address inside the same half, so the relay is still
handed exactly the numeric inverse job ({tokenOut: +amountOut,
tokenIn: -amountIn}).
BREAKING CHANGE: observable refusal behavior changes. Jobs with
`0 < amountOut < interpolated(amountIn)` that previously terminated with
`job-error reason=route_not_current` now settle, with the solver
retaining the surplus. Refusal of `amountOut > interpolated(amountIn)`,
of sizes outside the ladder, of non-positive amounts, and every existing
safety refusal (cache currentness, pair/minimum policy, DUST admission,
Stock affordability, exact-file identity) are preserved. Operators
should expect previously-refused lowered-demand jobs to consume maker
offers and settle.
The former strict-equality refusal test is re-encoded, not deleted: it
keeps the above-advertised half of its assertion and is renamed
accordingly, and the accepted half becomes a 12-row route-resolution
acceptance matrix plus four end-to-end executor cases (surplus with
residual input, lowered exact rung, minimum positive demand, residual
payout after lowering).
…ror budgets)
Remediates 00003 findings P4-F02, P4-F03 and P4-F04 (spec 00005 FR-002,
FR-003, FR-004). Before this, the solver advertised liquidity it would
then refuse after a taker's job had already been routed, and the relay
kept quoting the same unexecutable rung afterwards.
P4-F02 — the admission policy evaporated between config and the wire.
run.ts passed supportedPairs/minJobOutput into the relay client's ladder
options, RelayLadderOptions did not declare them, and runPush never
forwarded them, so pairs and sizes the executor refuses were published.
Fixed structurally rather than by adding two fields: a new
@zswap-da/solver-core/admission-policy module owns JobAdmissionPolicy,
ADMISSION_POLICY_FIELDS (with a compile-time exhaustiveness guard over
keyof JobAdmissionPolicy), forwardAdmissionPolicy() driven by that tuple,
and admissionPairKey(). DeriveLadderOptions, LadderPushOptions,
RelayLadderOptions, SwapJobExecutorOptions, resolveSwapJobRoute's options
and SolverAdmissionEnv all extend it, and every per-field spread on the
publication and admission paths is now one forwardAdmissionPolicy() call.
Adding a policy field is now one tuple edit; omitting it is a type error.
P4-F03 — publication had no residual-affordability input. deriveLadder
now takes a spendableInventory snapshot (Stock.available per token, read
once per push) and truncates a pair's rungs at the first one whose
interpolation interval could demand more tokenOut than the solver can
reserve. The requirement is the relay's own arithmetic,
floor(amountOut*(amountIn-1)/amountIn), not the whole-pair residualBound,
which would over-suppress. The first rung is exempt because it opens no
interval, so a solver holding zero tokenOut still publishes it.
P4-F04 — nothing proved the fee-sizing mirror could be funded. buildHalf
opens with initSwap({shielded: {[tokenIn]: amountIn}}), which selects real
coins for the taker's full input out of the solver's own wallet, so an
unfundable job failed part-way through a wallet mutation and an uncertain
revert then quarantined the job, stranding its claim and a capacity slot.
Publication now caps published rung inputs by Stock.available(tokenIn)
(interpolateQuote refuses any size above the last rung's input, so that
tail is the ceiling on a job's amountIn), and resolveSwapJobRoute refuses
JOB_ROUTE_UNAVAILABLE before Stock.reserve and therefore before any
journal row or wallet call. R1 made this more load-bearing: every
lowered-demand job builds the mirror too.
AVAILABILITY / COMPATIBILITY CHANGE — operators will see previously
advertised liquidity withdrawn. Concretely:
* a solver publishes only the ladder prefix its own tokenIn holdings
cover, so a solver holding no tokenIn publishes NOTHING for that pair
however deep the maker book behind it is. Deployments that funded the
solver with tokenOut only must now fund tokenIn as well;
* a rung whose worst-case interval residual exceeds available tokenOut
is withheld, together with every rung above it (a whole-offer
cumulative ladder can be truncated but not punctured);
* an emptied Stock — what an in-flight or failed balance refresh
produces — now withdraws every budget-bounded rung, not just residual
authority. run.ts therefore republishes immediately on both edges of
inventory readiness, so a withdrawal does not lag the executor's
refusals and a recovery does not cost a full push interval after each
settlement.
Withheld liquidity is reported as a loud operator signal
(ladder-budget-limited / ladder-budget-cleared, with counts), kept
separate from the existing cap-truncation signal because funding a wallet
and raising a limit are different remedies.
Budgets are per rung, not aggregated across pairs or concurrent jobs:
that is FR-003's wording, and Stock.reserve remains the atomic aggregate
authority at execution. Absent inventory is OPEN in the pure derivation
so dry-run and derivation tests keep their contract; the live push always
supplies a snapshot and the executor re-checks both numbers.
No safety behavior changes (FR-010): exact-file identity/currentness,
reservation ownership, journal/CAS recovery, quarantine, fail-closed
withdrawal and DUST admission are untouched, and R1's lower-output/
surplus semantics are unchanged.
Two pre-existing tests were amended, both forced by FR-004 and both
documented in place: R1's surplus test funded neither token and is now
"needs no tokenOut inventory" (keeping B at zero, which is the property it
exists for), and the two runSolver integration tests now fund tokenIn.
Tests: 34 new tests across admission-policy (new file), ladder-derivation,
ladder-source, relay-client, swap-job-executor, run and stock, covering
the full publication matrix on the wire including across a reconnect, both
budget boundaries, per-push budget reads, the operator signals, and
executor admission refusing each case independently. Space-free clone:
solver suites 429 -> 463 pass / 0 fail; CI unit-tests command 898 -> 932
pass with the same single pre-existing docker-load flake; typecheck:backend
0 diagnostics with the dependency count unchanged at 23; check:pgtypes and
the docs/api-examples playground unchanged. A strict/noEmit probe over the
solver roots goes 57 -> 56 diagnostics, the one removal being F02's own
run.ts TS2353 — no new strict diagnostics are introduced.
Closes 00003 P4-F05 and P4-F06 (spec 00005 FR-005, FR-006). FR-005 — one explicit solver component. New root `start.solver.ts` (`bun run start:solver`) runs the solver as a single process with no orchestrator: it attaches to an already-running kernel API and Midnight Intents relay. The whole configuration contract lives in the new `packages/solver/src/launch.ts`, which is side-effect free and aggregates EVERY problem into one error instead of failing on the first, before any wallet, socket or journal is touched. Mandatory: MIDNIGHT_NETWORK_ID, ZSWAP_API, SOLVER_RELAY_WS_URL, SOLVER_RELAY_HTTP_URL, SOLVER_RELAY_AUTH_TOKEN (>= 32 chars), SOLVER_JOURNAL_PATH (absolute; :memory: impossible) and SOLVER_SEED — in dry-run as well as live mode, because a rehearsal that leaves half the configuration unvalidated is not one. Three of those have silent developer defaults a deployment must not inherit (the SDK assumes `undeployed`, the API assumes 127.0.0.1:9999, the seed assumes the repository's public dev seed, which is now accepted only on `undeployed`). Mainnet keeps the hardened boundary: SOLVER_DRY_RUN defaults to true there and live settlement still requires the exact SOLVER_MAINNET_LIVE_TRADING_ACK=true, so this entrypoint is not a cheaper route to live trading. The startup banner prints the topology and no secret. SOLVER_ENABLED=false exits 0 without demanding any of it. `start.mainnet.ts` is deliberately UNCHANGED: adding the solver to `start:mainnet` would turn a backend command into a trading command (00003 P5-D02). The per-network entrypoints are untouched. FR-006 — the gate that would have caught this class. New `scripts/solver-typecheck.ts` (`bun run typecheck:solver`, also under a new umbrella `bun run typecheck`, plus a `solver-typecheck` CI job mirroring the backend one). Strict/no-emit over packages/solver, packages/solver-core and packages/validator — production AND test sources — plus every first-party file that imports the solver's source, discovered by scanning rather than from a maintained list (a list is the failure mode P4-F02 already showed). 97 roots, 0 diagnostics, from 65 before this commit. Notable repairs behind that zero: - swap-job-executor: the re-entering awaiting record re-states its narrowed walletTransaction, and `record.reverting` becomes an `in`-guard, which is precisely the previous runtime meaning. No behavior change. - solver-core/wallet.ts: the bech32m cast is documented as a duplicate-scope install artifact (@midnight-ntwrk 3.1.0 vs @midnightntwrk 3.1.2); importing the facade's codec instead would change runtime. - validator: the proof-variant diagnostics 00003 recorded, fixed the way solver-core/api-client.ts already did it. - the legacy grand-E2E solver harness is REPAIRED, not deleted: it is live E1 infrastructure (a unit test imports it, the E1 driver execs it). What was stale is deleted — the `onValidationTrace`/`onOutcome`/`onMatchOutcome` options it passed to runSolver, which SolverOptions has not had since N5 and which were therefore never invoked, plus the ValidationGateTrace type, the recordRealValidationTraceEvidence helper they fed, the offer cache only they populated, and the one test that existed only to drive that helper. Runtime-neutral; a live evidence source is N6's replacement work. Existing safety behavior is preserved (FR-010): exact-file identity and currentness, reservation ownership, journal/CAS recovery, quarantine, fail-closed withdrawal and DUST admission are untouched. Tests: 25 new in packages/solver/launch.test.ts, two of which drive the real root entrypoint as a child process (missing configuration exits 1 listing all seven variables; SOLVER_ENABLED=false exits 0). One grand-E2E test removed with its dead helper. `bun run typecheck:solver` 0 diagnostics, `bun run typecheck:backend` 0 diagnostics (its out-of-gate dependency count drops 23 -> 21 from the validator/wallet fixes), `bun run check:pgtypes` pass, docs playground `tsc -b` and the five api-example bundles exit 0.
Closes 00003 P4-F11's documentation subset (spec 00005 FR-007). README.md — the solver was absent from the Environments table, the project structure and the key-files table, which is half of what made the production topology ambiguous (P4-F05). It now has a row in every one of them, plus a "Running the COW solver" section: `bun run start:solver`, the seven mandatory variables with the reason each one is mandatory, the dry-run/mainnet-ack boundary, the container note, and an explicit statement that neither orchestrator launches the solver and why. Services & ports records that the solver listens on nothing — it is outbound only. Three semantic corrections in that section, each labelled as a change rather than folded into prose: - Supported domain: Midnight 1.x / ledger-v8, single-leg distinct-token shielded offers, at most 8 makers per job, optional shielded residual. Everything else is refused before admission, not partially supported. - Fees: maker offers are built `payFees:false`, so the settling side pays; the solver sizes and funds DUST for what it submits, under the SOLVER_DUST_* budget. - BEHAVIOR CHANGE (from the lower-exact-output commit): a job's amountOut is the taker's exact demand and may sit below the advertised interpolation. Every job with 0 < amountOut <= interpolate(amountIn) now settles, with the maker-prefix difference either paid from inventory (residual) or retained by the solver (surplus). Demands above the advertised output stay refused. Deployments that relied on the old `route_not_current` refusal will see those jobs settle. - AVAILABILITY CHANGE (from the executable-liquidity commit): published liquidity is bounded by what the solver can execute — unaffordable-residual rungs and everything above them are withheld, rung inputs are capped by provably spendable tokenIn (so a solver holding no tokenIn publishes nothing for that pair), the pair/minimum policy now bounds publication as well as admission and survives reconnects, and withheld depth is reported as ladder-budget-limited / ladder-budget-cleared. Operators must fund the solver wallet with BOTH tokens of every pair it quotes. API.md — "Repository validation scope" now describes both strict gates and why the solver gate includes test sources; the solver environment block is corrected (the stale "dry-run does NOT load inventory" line was false since RF3) and completed with the mandatory and policy variables; and a new "The COW solver (Midnight Intents side)" section carries the topology, supported domain, fee ownership, indicative-quote semantics, the job-disposition table and the executability bounds. docs/protocol-scope.md — the unapproved 2026-08-13 scope draft 00003 flagged. It describes a public offer-book market maker that submits directly, which is not this system. Retired in place: a SUPERSEDED banner naming the contradiction and pointing at README/API.md, a Superseded row in its status table, and inline notes on the three decisions affected. Kept rather than deleted because its default-off safety posture is still the one in force and the record of why these decisions were made has value; nothing in the repo references it. .env.mainnet.example — a start:solver block listing what that entrypoint requires, ZSWAP_API, and a "fund both sides" note on SOLVER_SEED. Documentation only; no source or test file is touched by this commit.
… relay, chain, scripts)
Adds `deploy/`: one Docker Compose project in which every component is its own
service running a single process (spec FR-008 / SC-004 static half). The in-repo
orchestrator is never the launcher — `launchMidnight()`/`launchCelestia()` open
by killing whatever holds 9944/8088/6300/26657/26658/3334 (`stopProcessAtPort`),
which in this topology is the sibling services, and their deploy step would mint
a second contract identity while every volume still referred to the first.
Services: midnight-node, proof-server, indexer, celestia, pglite,
offerfiles-deploy (one-shot), kernel, batcher, relay, solver, scripts (profile
`e2e`). One kernel image with six entrypoints; the relay built from the
UNMODIFIED pinned reference.
Chain versions are not chosen here — they are read off this repo's own pinned
dependencies, i.e. what `bun run dev` already runs, all from
effectstream/binaries 0.3.120 and sha256-verified at build time:
midnight-node 1.0.0 @effectstream/npm-midnight-node@0.103.1
proof-server ledger-8.1.0 @effectstream/npm-midnight-proof-server@0.103.1
indexer-standalone v4.3.3 @effectstream/npm-midnight-indexer@0.103.1
celestia-appd v6.4.10 @effectstream/celestia@0.103.1
celestia-node v0.28.4 @effectstream/celestia@0.103.1
Compact CLI 0.30.0 start.dev.ts + contract-offer-files/package.json
bun 1.3.11 the runtime this branch was developed on
Notable decisions, each argued in the file that makes it:
* Celestia runs linux/amd64 (emulated on arm64): release 0.3.120 ships no
celestia linux/arm64 asset. Both binaries were verified to execute under
emulation on this host. Building from upstream Go source is the recorded
fallback, but it would run different binaries than the ones we pin.
* Chain images are on debian:trixie-slim, and each one now proves at BUILD time
that its binary links and starts. celestia-appd v6.4.10 and
indexer-standalone v4.3.3 need GLIBC 2.38, which bookworm (2.36) lacks — a
failure that otherwise only appears minutes into a bring-up.
* A `pglite` service is required. `PGLITE=true` does not mean "embedded":
`launchPglite()` spawns a pg-gateway TCP server wrapping PGlite that the
kernel dials over the Postgres wire protocol.
* No celestia auth token and no shared auth volume: the bridge runs with
`--rpc.skip-auth`, exactly as @effectstream/celestia does.
* The relay is built via a named build context (`reference`), so every line that
reads reference source is marked `--from=reference`. The recipe is a faithful
transcription of the reference's own infra/Dockerfile.relay; it is transcribed
rather than used in place so build-side fixes land here and never there.
* The solver entrypoint deliberately validates nothing about the solver's own
configuration. `start.solver.ts` reports all seven missing boundaries at once
and exits 1; a pre-check would shadow the negative control SC-004 asks for.
* The kernel entrypoints drop empty OPTIONAL variables. Compose renders an unset
variable as "", and `getEnv(x) ?? default` treats "" as a real value — a blank
knob in .env would otherwise silently mean an empty Celestia namespace or an
empty ladder path. The seven mandatory solver variables are excluded.
Shared-host rules are structural: every published port comes from .env, defaults
to >= 10000, and binds ${BIND_ADDR} (127.0.0.1); COMPOSE_PROJECT_NAME namespaces
everything; ./down.sh removes containers, networks AND volumes (all chain-keyed,
so they must go as a set) and prints the proof.
.gitignore gains a scoped un-ignore for deploy/**: the pre-existing "Preview
Docker test setup" block ignores `Dockerfile`, `.dockerignore` and `.env` at any
depth, which would have silently swallowed this deliverable. deploy/.env stays
ignored; only deploy/.env.example is committed.
D1.3 gates: 16/16 pass (compose config renders; all six images build; five
solver fail-fast negative controls driven in-container against the real
entrypoint; disabled-solver clean exit; entrypoints parse; pglite resolver;
Compact artifacts present). Full teardown verified: zero containers, volumes,
networks, images.
Bring-up of the whole stack is D2 and has NOT been performed.
D2 bring-up surfaced three defects that a green build could not catch, plus
the silent empty-ladder trap R2 warned about. All fixes are in deploy/; no
production source and no reference-clone file is touched.
midnight-node could not start. The 0.3.120 release zip is not just a binary:
it carries a `res/` tree (res/cfg/*.toml, res/genesis/*, chain specs) and the
node resolves its config as current_dir()/res/cfg/ -- CFG_ROOT is only ever
set programmatically. D1's image installed the binary and deleted the rest, so
the node panicked at startup with "failed reading default.toml at path
/res/cfg/default.toml". @effectstream/npm-midnight-node hides the requirement
by spawning with cwd set to its extracted directory; nothing in the flag set
hints at it, which is why the `--version` build gate passed on a broken image.
Now res/ is installed to /opt/midnight-node, asserted at build time relative to
a new WORKDIR, and the entrypoint cd's there after checking -- exiting 78 with
an explanation instead of a Rust panic. The proof-server and indexer assets
were checked and are single-file zips, so neither shares the defect.
The stack came up entirely healthy and quoted nothing. Since R2, publication
is bounded by the solver's own spendable inventory, and the ladder derives
from the mirrored maker book -- so an unfunded solver with an empty book
publishes empty capabilities and empty levels forever, with nothing logged as
an error anywhere. Two one-shots now provision both halves, following the
offerfiles-deploy pattern (restart: no, marker-idempotent, downstream
service_completed_successfully), and both fail LOUDLY:
solver-provision runs the repo's own packages/solver/scripts/bootstrap-dev.ts
-- funds solver NIGHT from genesis, registers dust, mints
the solver BOTH tokens of the pair, and regenerates the
ladder config with this chain's real colors. `solver` waits
on its completion, which is not mere ordering: it drives a
wallet on SOLVER_SEED, and two facades on one seed against
one node force each other's connection down.
maker-offer posts ONE real, proven, settle-able offer into the kernel
book via the new deploy/scripts/post-maker-offer.ts.
post-maker-offer.ts exists because neither documented path works: `seed:market`
writes rows whose blob is a placeholder and which its own header calls NOT
settle-able (a ladder built on those would let the smoke pass over a stack that
can never fill anything), and api-examples/10-submit-offer.ts is stale against
the wallet SDK -- it passes a STRING where receiverAddress.coinPublicKey is now
dereferenced, dying with "undefined is not an object". This follows the
maintained path (packages/tests/two-wallet-swap-e2e.ts) and uses the address
OBJECT from wallet.shielded.getAddress(). The api-example itself is source
scope and is left alone; recorded as Q-D2-1.
Token colors derive from the deployed contract address, so they are new on
every stack and mint-test-tokens only PRINTS them. entrypoint-deploy.sh now
publishes minted-tokens.json atomically onto the shared volume beside the
contract address, so downstream provisioning names tokens from a durable
artifact instead of scraping a pruned one-shot's logs.
SOLVER_LADDER_CONFIG now points at the generated ladder on a new chain-keyed
solver-config volume; the checked-in ladders.dev.json names colors from an
older deployment. Publication ignores that file, but engine.ts's direct-fill
path reads it via ladders.maxPayout().
RISK-1 (indexer /api/v3 vs /api/v4) is resolved and was a non-issue:
indexer-standalone v4.3.3 serves the SAME schema at both paths (identical
__schema field sets, both /ws endpoints upgrade, and any other /api/<x>/graphql
308s to /api/v4/<x>/graphql). Each side now uses the path its own codebase
treats as canonical. No code change either way.
Verified: clean-state `docker compose up -d` exits 0 with all services healthy
and three one-shots exited 0; the solver connects to the UNMODIFIED reference
relay and publishes a non-empty ladder observable at the relay -- POST /quote
returns amountOut=500000 for amountIn=750000, exactly the maker offer's terms,
with directional and unknown-pair negative controls returning 503. Static
gates 16/16. Fail-fast negative controls pass through both the real service
entrypoint and the launch contract directly.
…ce relay
Adds the E1 driver (spec 00005 FR-009 / SC-005) and the one deployment fix that
made settlement possible at all. Everything is inside `deploy/`; no product
source is touched.
deploy/scripts/e2e.ts drives four cases against the UNMODIFIED reference relay,
each with its own maker offer because a settled case consumes the one it used:
A demand = the relay's own quote -> settles
B demand = quote x 4/5, strictly BELOW it -> settles, solver keeps
the surplus
C demand = quote x 6/5, ABOVE it -> refused, nothing moves
D demand = quote, on the offer C was refused -> settles
B is the real-boundary proof for FR-001 / P4-F01, and it is producible on the
real wire because POST /intent reads the demand out of the taker's own
transaction deltas (TxProcessor.extractQuote) rather than from the quote it
issued: a solver qualifies when interpolateQuote(levels, dx) >= requiredOutput,
so a lower demand is forwarded verbatim and a higher one leaves no qualifying
solver. quoteId pins the solver, not the amount.
D is C's control. Without it, "the relay refused" and "the relay was accepting
nothing right then" are the same observation, because a bare 503 at /intent is
also what a ladder withdrawal window answers.
FIX: the devnet's Celestia block cadence made settlement impossible.
@effectstream/celestia starts celestia-appd with --delayed-precommit-timeout 1s
(~1 block/s), against which the kernel's Celestia projection holds a persistent
~11-block deficit — while deriveSyncStatus calls anything past
MAX_CELESTIA_LAG_BLOCKS = 4 "syncing" and requireCurrentBackend refuses. So
POST /v1/offers/files, the read the solver makes at job time to rebuild a
maker's exact bytes, answered 60/60 503 FILES_UNAVAILABLE and every dispatched
swap died exact_files_unavailable — with GET /v1/health reporting "ok", because
its cached external tips are up to 60 s old. entrypoint.sh now takes
CELESTIA_BLOCK_TIME (default 3s, the value this repo's own acceptance harness
pins for exactly this reason, citing its e2e open question E1-Q4), and the
kernel/batcher get CELESTIA_POLLING_INTERVAL_MS=1000. After the change the same
probe is 20/20 x 200.
Also: the Compact compile's inputs are copied ahead of the rest of the source in
the kernel image, so editing a script no longer invalidates proving-key
generation.
Recorded for the user as Q-E1-1: `bun run dev` still produces a stack on which
no swap can settle.
acedward
marked this pull request as ready for review
September 1, 2026 17:53
acedward
added a commit
that referenced
this pull request
Sep 2, 2026
…ree-fee-sizing feat(solver): capital-free fee sizing — lift the tokenIn publication cap (follows #52)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #48 (base:
feat/cow-solver). Makes the COW solver conformant with theshieldedtech/midnight-intents-swapsPhase 1 relay (pinned061f4d3), remediating the findings of the independent 00003 static conformance assessment, and adds a split-component Docker Compose deployment plus a cross-stack E2E that proves settlement end to end.The solver previously required a dispatched job's
amountOutto equal the interpolated ladder output exactly and refused reference-valid jobs asroute_not_current. It now accepts any job with0 < amountOut <= interpolated(amountIn), pays the taker exactlyamountOut, and retains the surplus — matching the reference relay's admission (output >= requiredOutput) and the reference solver's pay-exact-demand behavior. Jobs that used to terminatejob-error route_not_currentnow settle and consume maker offers. All other refusals (above-advertised, out-of-ladder, currentness, pair/minimum policy, DUST, Stock, exact-file identity) are preserved. (ef4b222)One typed
JobAdmissionPolicy(supportedPairs,minJobOutput) now flows whole from config through publication to executor admission (it was previously dropped atRelayLadderOptions, so the solver advertised pairs it refused). Ladder publication is now bounded by per-push Stock budgets: rungs whose interpolation interval could demand unaffordable residual tokenOut are withheld, and published rung inputs are capped by spendable tokenIn (the fee-sizing mirror must spend the fullamountIn). Executor admission re-checks and refusesJOB_ROUTE_UNAVAILABLEbefore any journal row or wallet mutation. Operators will see previously advertised liquidity withdrawn — a solver holding no tokenIn publishes nothing for that pair, however deep the maker book. Withheld liquidity is reported via newladder-budget-limited/ladder-budget-clearedevents. (c4ac2bb)Explicit solver topology + strict typecheck gate (P4-F05/F06/F11-docs)
bun run start:solver(start.solver.ts+ side-effect-freepackages/solver/src/launch.ts): a single process that validates all seven mandatory env boundaries in one pass and exits non-zero listing every problem before touching a wallet, socket, or journal.start:mainnetdeliberately still does not launch the solver (no-auto-trading safety); the mainnet dry-run +SOLVER_MAINNET_LIVE_TRADING_ACKboundary is preserved. (360d216)bun run typecheck:solverCI gate: strict/no-emit overpackages/solver,packages/solver-core,packages/validator(tests included) plus every first-party file importing solver source — 0 diagnostics across 98 roots (from 65). The legacy grand-e2e harness was repaired by deleting threerunSolvercallbacks removed since N5 (runtime-neutral). (360d216)payFees:falsefee ownership, and both changes above; the supersededdocs/protocol-scope.mdis retired in place. (ff69c88)Split-component Docker Compose deployment (
deploy/)13 services, every component its own single-process container — the in-repo orchestrator launches nothing:
midnight-node1.0.0,proof-serverledger-8.1.0,indexer4.3.3,celestia6.4.10/0.28.4 (all sha256-pinned fromeffectstream/binaries0.3.120),pglite,offerfiles-deploy(one-shot),kernel,batcher,relay(built from unmodifiedmidnight-intents-swapssource),solver, plus devnet-gated provisioning one-shots and an E2E scripts service. All host ports parameterized ≥ 10000 on 127.0.0.1. (7357513,a38669e)Note: this required a scoped
.gitignoreexception (!deploy/**) — the pre-existing root patterns ignoreDockerfile/.env/.dockerignoreat any depth and would have swallowed the deliverable.Cross-stack E2E (
deploy/scripts/e2e.ts)Against the running stack and the unmodified reference relay — 51 assertions, exit 0:
POST /quote+POST /intent→ solver settles consuming the kernel offer; taker credited exactly 500000 for 750000 in; offer consumed on-chain.503 no_solver, zero wallet mutations, zero journal rows — with D as the control settling the very offer C was refused against.One blocker found en route was a kernel devnet-config defect, not a solver defect: the packaged 1 s Celestia cadence keeps the projection outside
MAX_CELESTIA_LAG_BLOCKS=4, so no swap can settle on a stockbun run devstack. Fixed insidedeploy/(3 s cadence); the dev launcher itself is left for follow-up.Verification
typecheck:solver0/98,typecheck:backend0,check:pgtypespass.Known limitation:
revertTransactionon a no-coin leg is implemented but unexercised (failure path never hit in E2E).