dexkeeper: CI image build + devnet deploy manifests (KMSSecret + Deployment + drive-swap Job)

- Dockerfile.dexkeeper: pure-Go (CGO off) static keeper image; documents the
  zap>=v0.8.10 + chains-RPC-seam tag prereqs.
- docker.yml: add ghcr.io/luxfi/dexkeeper to the build matrix.
- k8s/devnet-keeper/kmssecret-mnemonic.yaml: SANCTIONED maker-key path — a KMSSecret
  (operator-reconciled) that syncs the devnet mnemonic from lux KMS (lux-infra/devnet,
  /staking key=mnemonic, the proven luxd-staking-kms-sync scope) into LUX_MNEMONIC. No
  plaintext handled out-of-band. Server-dry-run validated against the live CRD.
- deployment.yaml: the keeper in watch mode against devnet C/D RPC + dchain-venue:9099,
  maker key from the synced secret, single replica (proposer-relays-once).
- job-drive-first-swap.yaml: one-shot seed + Phase-A swap to kick the first DEXFill.
- README.md: the full deploy sequence + verification (DEXFill eth_getLogs, subgraph, UI).

DEVNET ONLY. All manifests dry-run clean.
This commit is contained in:
2026-06-22 02:30:40 -07:00
parent e71230aca7
commit 85ce5f482c
5 changed files with 239 additions and 0 deletions
+2
View File
@@ -19,6 +19,8 @@ jobs:
dockerfile: Dockerfile
- image: ghcr.io/luxfi/dex-gateway
dockerfile: Dockerfile.gateway
- image: ghcr.io/luxfi/dexkeeper
dockerfile: Dockerfile.dexkeeper
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
with:
image: ${{ matrix.image }}
+41
View File
@@ -0,0 +1,41 @@
# syntax=docker/dockerfile:1
#
# dexkeeper — the off-chain keeper that drives the on-chain 0x9999 two-phase atomic
# DEX seam (watch IntentSubmitted -> dexvm ImportTx + RelayOrderTx via dex.submitTx ->
# poll dex.getSettlement -> C-side Phase-B settle -> DEXFill). Pure-Go (CGO disabled);
# the maker key is derived in-process from the LUX_MNEMONIC env (KMS-synced secret).
#
# BUILD DEP NOTE: the keeper imports github.com/luxfi/zap (>= v0.8.10, the
# EncodeIntentHookData/EncodeSettlementHookData calldata) and github.com/luxfi/chains
# (the dexvm txs + the dex.submitTx/getSettlement RPC server). Both tags must be cut
# before this image builds from published modules alone (go.work is NOT used in CI).
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
RUN apk add --no-cache git ca-certificates
ARG GITHUB_TOKEN
RUN git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/"
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ARG TARGETARCH=amd64
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -ldflags="-s -w" -o dexkeeper ./cmd/dexkeeper
FROM alpine:3.21
RUN apk add --no-cache ca-certificates && \
addgroup -g 1000 -S keeper && \
adduser -u 1000 -S keeper -G keeper
WORKDIR /home/keeper
COPY --from=builder /app/dexkeeper .
RUN chown keeper:keeper dexkeeper
USER keeper
# No ports: the keeper is an egress-only client (C-Chain RPC + dexvm RPC + venue ZAP).
ENTRYPOINT ["./dexkeeper"]
+66
View File
@@ -0,0 +1,66 @@
# dexkeeper — devnet 0x9999 DEX-seam deploy
The keeper that makes the on-chain **0x9999 two-phase atomic DEX swap flow**
operational on **devnet** so the first real `DEXFill` lands and the V4/DEX markets
populate. DEVNET ONLY.
## The seam (what the keeper drives)
```
C Phase-A intent (signed swap to 0x9999, DI01 hookData)
-> SubmitSwapIntent locks input + stages C->D object (key=intentID) + emits IntentSubmitted
KEEPER watches IntentSubmitted
-> dexvm ImportTx (consume C->D object, fund D order) via dex.submitTx
-> dexvm settling RelayOrderTx (clob_submit, CollateralRef=intentID) via dex.submitTx
dexvm proposer relays ONCE (obtainFills) -> fills carried -> settleFromFills exports D->C proceeds
KEEPER polls dex.getSettlement(intentID) -> {outputID, amount}
-> C Phase-B settle (signed swap to 0x9999, DS01 outputID|amount|intentID)
-> ImportSettlement credits maker + emits DEXFill <-- the goal
```
## Prerequisites (status)
1. **dexvm relay wired** — DONE. All 5 `lux-devnet` validators log
`relayConfigured:true -> dchain-venue:9099` (per-chain config.json seeded by the
`luxd-startup` CM; mirrored to source in `universe` `feat/dexvm-relay-devnet`).
2. **dexvm RPC seam** — code in `chains` `feat/dexvm-submit-tx-rpc`: `dex.submitTx`
(mempool entry) + `dex.getSettlement` (proceeds-coordinate query). The luxd node
image must be rebuilt with the new `dexvm-linux-amd64` plugin (S3) and the
validators rolled onto it.
3. **keeper image** — `ghcr.io/luxfi/dexkeeper` (this repo, `docker.yml` matrix +
`Dockerfile.dexkeeper`). Requires `zap >= v0.8.10` and a `chains` tag carrying the
RPC seam to be cut first (the keeper imports both).
4. **maker key in KMS** — the devnet mnemonic is already in lux KMS; `kmssecret-mnemonic.yaml`
syncs it to the `dexkeeper-mnemonic` secret. The keeper derives the maker
(`0x9011…`, `m/44'/9000'/0'/0/i`) in-process.
## Deploy sequence (CI/CD + operator only)
```sh
# 0) Cut tags so the keeper image builds from published modules:
# - luxfi/zap v0.8.10 (EncodeIntentHookData / EncodeSettlementHookData)
# - luxfi/chains <tag> (dexvm dex.submitTx + dex.getSettlement)
# 1) Rebuild the node image + dexvm-linux-amd64 plugin (chains tag), publish to S3,
# bump the lux-devnet LuxNetwork CR pluginSource, roll the 5 luxd pods (OnDelete).
# 2) Build the keeper image (push to main / tag v* triggers docker.yml).
# 3) Sync the maker key + deploy the keeper:
kubectl --context do-sfo3-lux-k8s apply -f kmssecret-mnemonic.yaml
kubectl --context do-sfo3-lux-k8s apply -f deployment.yaml
# 4) Seed venue liquidity + drive the first swap:
kubectl --context do-sfo3-lux-k8s apply -f job-drive-first-swap.yaml
```
## Verify
```sh
# first DEXFill on devnet C (report the tx hash):
curl -s -X POST https://api.lux-dev.network/ext/bc/C/rpc -H 'content-type:application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getLogs","params":[{"address":"0x0000000000000000000000000000000000009999","fromBlock":"0x0","toBlock":"latest"}]}'
# DEXFill topic0 = keccak256("DEXFill(bytes32,address,uint256,uint256)")
# dex subgraph shows the market + fill (not markets:null):
curl -s -X POST https://explorer.lux-dev.network/v1/graph/cchain/dex/graphql \
-H 'content-type:application/json' -d '{"query":"{ markets { id baseAsset quoteAsset } fills(first:3){ id } }"}'
# UI: lux.exchange?network=devnet renders the V4/DEX market.
```
+68
View File
@@ -0,0 +1,68 @@
# dexkeeper Deployment (lux-devnet): the off-chain keeper that drives the on-chain
# 0x9999 two-phase atomic DEX seam so DEXFill lands on the devnet C-Chain.
#
# It watches the C-Chain 0x9999 IntentSubmitted event, submits the dexvm ImportTx +
# settling RelayOrderTx (via dex.submitTx), polls dex.getSettlement for the D->C
# proceeds object, then has the maker sign + send the C-side Phase-B settle (which
# emits DEXFill). The maker key is derived in-process from LUX_MNEMONIC (KMS-synced;
# see kmssecret-mnemonic.yaml).
#
# PRECONDITIONS (all in this work's branches):
# - luxd validators on a node image whose dexvm includes dex.submitTx + dex.getSettlement
# (chains feat/dexvm-submit-tx-rpc) and relayConfigured:true (done — luxd-startup CM).
# - dchain-venue:9099 reachable (done).
# - dexkeeper image built (ghcr.io/luxfi/dexkeeper, docker.yml matrix).
apiVersion: apps/v1
kind: Deployment
metadata:
name: dexkeeper
namespace: lux-devnet
labels:
app.kubernetes.io/name: dexkeeper
app.kubernetes.io/part-of: lux-dex
spec:
replicas: 1 # single keeper: relays are proposer-relays-once; a 2nd keeper would
# double-submit ImportTx/RelayOrderTx (idempotent on the dexvm side by
# content-addressed UTXO/receipt, but no need). Scale only with leader-election.
selector:
matchLabels:
app.kubernetes.io/name: dexkeeper
template:
metadata:
labels:
app.kubernetes.io/name: dexkeeper
app.kubernetes.io/part-of: lux-dex
spec:
imagePullSecrets:
- name: ghcr-luxfi
containers:
- name: dexkeeper
image: ghcr.io/luxfi/dexkeeper:latest
imagePullPolicy: IfNotPresent
args:
- --mode=watch
- --c-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/C/rpc
- --d-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX/rpc
- --d-chain-id=28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX
- --venue=dchain-venue.lux-devnet.svc.cluster.local:9099
- --maker=0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
env:
# The BIP-39 mnemonic the keeper derives the maker key from (KMS-synced).
- name: LUX_MNEMONIC
valueFrom:
secretKeyRef:
name: dexkeeper-mnemonic
key: LUX_MNEMONIC
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop: ["ALL"]
@@ -0,0 +1,62 @@
# One-shot Job: seed venue liquidity + drive the FIRST Phase-A 0x9999 swap from the
# maker, in-cluster (the maker key is derived from the KMS-synced LUX_MNEMONIC; nothing
# is handled out-of-band). The running dexkeeper Deployment then drives Phase B -> the
# first DEXFill. Use this to seed + kick the first trade; delete after it Completes.
#
# Modes (--mode):
# seed — only seed the venue (clob_deposit + maker resting ask) over ZAP.
# phase-a — seed + send ONE Phase-A intent swap from the maker; prints intentID + txHash.
#
# After this Completes, verify:
# eth_getLogs(address=0x9999, IntentSubmitted) on devnet C returns the intent;
# the dexkeeper logs show ImportTx + RelayOrderTx submitted + Phase-B settle;
# eth_getLogs(address=0x9999, DEXFill) returns the first fill.
apiVersion: batch/v1
kind: Job
metadata:
name: dexkeeper-drive-first-swap
namespace: lux-devnet
labels:
app.kubernetes.io/name: dexkeeper
app.kubernetes.io/part-of: lux-dex
spec:
ttlSecondsAfterFinished: 600
backoffLimit: 1
template:
metadata:
labels:
app.kubernetes.io/name: dexkeeper
spec:
restartPolicy: Never
imagePullSecrets:
- name: ghcr-luxfi
containers:
- name: dexkeeper
image: ghcr.io/luxfi/dexkeeper:latest
imagePullPolicy: IfNotPresent
args:
- --mode=phase-a
- --c-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/C/rpc
- --d-rpc=http://luxd-0.luxd-headless.lux-devnet.svc.cluster.local:9650/ext/bc/28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX/rpc
- --d-chain-id=28mZwmZtKAdGWoJRWSYw8RXxUcHZUDVsNgjMH4nnMfoJGJi2SX
- --venue=dchain-venue.lux-devnet.svc.cluster.local:9099
- --maker=0x9011E888251AB053B7bD1cdB598Db4f9DEd94714
env:
- name: LUX_MNEMONIC
valueFrom:
secretKeyRef:
name: dexkeeper-mnemonic
key: LUX_MNEMONIC
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop: ["ALL"]