From 0944deaf7738060155b83844b0599fae9520965c Mon Sep 17 00:00:00 2001 From: JP Date: Wed, 21 Jan 2026 22:45:35 +1100 Subject: [PATCH 1/2] fix: DCA plugin queue routing and policy billing fetch Two bugs fixed: 1. run-services.sh: Changed SERVER_TASKQUEUENAME to TASK_QUEUE_NAME - DCA server config uses envconfig with TASK_QUEUE_NAME - Wrong env var caused tasks to enqueue to default_queue - DCA worker never received reshare tasks, causing keyshares to be stored in wrong MinIO bucket (vultisig-verifier instead of vultisig-dca) 2. policy_generate.go: Use public /plugins endpoint for billing - Previous code used /plugin/{id} which requires auth - Auth failure caused empty billing array - Empty billing caused "billing policies count (0) does not match plugin pricing count (2)" error on policy creation - Now fetches from public /plugins endpoint and filters by ID Co-Authored-By: Claude Opus 4.5 --- local/cmd/vcli/cmd/policy_generate.go | 93 ++++++++++++++++++++++----- local/scripts/run-services.sh | 2 +- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/local/cmd/vcli/cmd/policy_generate.go b/local/cmd/vcli/cmd/policy_generate.go index 8cd9b06..3737e6f 100644 --- a/local/cmd/vcli/cmd/policy_generate.go +++ b/local/cmd/vcli/cmd/policy_generate.go @@ -123,22 +123,17 @@ func runPolicyGenerate(pluginID, from, to, amount, frequency, vaultName, toVault return fmt.Errorf("recipe validation failed: %w", err) } - // Include default free billing entries (required by DCA plugin) - // Both "once" and "per-tx" types are needed to match plugin pricing + // Fetch plugin pricing to build matching billing entries + billing, err := fetchPluginBilling(pluginID) + if err != nil { + // If we can't fetch pricing, use empty billing (plugin may not have pricing) + billing = []map[string]any{} + } + + // Build policy with recipe and billing policy := map[string]any{ - "recipe": recipe, - "billing": []map[string]any{ - { - "type": "once", - "amount": "0", - "asset": "usdc", - }, - { - "type": "per-tx", - "amount": "0", - "asset": "usdc", - }, - }, + "recipe": recipe, + "billing": billing, } jsonBytes, err := json.MarshalIndent(policy, "", " ") @@ -234,3 +229,71 @@ func extractErrorMessage(body []byte) string { } return string(body) } + +// fetchPluginBilling fetches the plugin's pricing and converts it to billing entries. +// The billing entries must match the plugin's pricing count for policy creation to succeed. +// Uses uint64 for amount to match verifier's expected type. +// Uses the public /plugins endpoint (no auth required) instead of /plugin/{id} (requires auth). +func fetchPluginBilling(pluginID string) ([]map[string]any, error) { + cfg, err := LoadConfig() + if err != nil { + return nil, err + } + + resolvedID := ResolvePluginID(pluginID) + + url := fmt.Sprintf("%s/plugins", cfg.Verifier) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch plugins: status %d", resp.StatusCode) + } + + var result struct { + Data struct { + Plugins []struct { + ID string `json:"id"` + Pricing []struct { + Type string `json:"type"` + Frequency *string `json:"frequency"` + Amount int64 `json:"amount"` + Asset string `json:"asset"` + } `json:"pricing"` + } `json:"plugins"` + } `json:"data"` + } + + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, err + } + + var targetPricing []struct { + Type string `json:"type"` + Frequency *string `json:"frequency"` + Amount int64 `json:"amount"` + Asset string `json:"asset"` + } + for _, p := range result.Data.Plugins { + if p.ID == resolvedID { + targetPricing = p.Pricing + break + } + } + + var billing []map[string]any + for _, p := range targetPricing { + entry := map[string]any{ + "type": p.Type, + "asset": p.Asset, + "amount": uint64(p.Amount), + } + billing = append(billing, entry) + } + + return billing, nil +} diff --git a/local/scripts/run-services.sh b/local/scripts/run-services.sh index d268d09..9b98f86 100755 --- a/local/scripts/run-services.sh +++ b/local/scripts/run-services.sh @@ -151,7 +151,7 @@ cd "$ROOT_DIR/app-recurring" export MODE="swap" export SERVER_PORT="8082" export SERVER_HOST="0.0.0.0" -export SERVER_TASKQUEUENAME="dca_plugin_queue" +export TASK_QUEUE_NAME="dca_plugin_queue" export SERVER_ENCRYPTIONSECRET="dev-encryption-secret-32b" export POSTGRES_DSN="postgres://vultisig:vultisig@localhost:5432/vultisig-dca?sslmode=disable" export REDIS_URI="redis://:vultisig@localhost:6379" From 60ca595b5cb4fe1faa08664812bb2d672541b7a4 Mon Sep 17 00:00:00 2001 From: JP Date: Thu, 22 Jan 2026 13:31:52 +1100 Subject: [PATCH 2/2] fix: E2E swap success - queue routing and billing fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes that enabled successful E2E swap test: 1. Production overlay updates: - app-recurring/server v1.0.84 (has TaskQueueName fix) - vcli v1.0.3 (has billing fetch fix) - nodeSelector removal patches for infra StatefulSets 2. policy_generate.go billing fix: - Added missing 'frequency' field to billing entries - Handle nil frequency pointer correctly 3. DCA server.yaml: - Added SERVER_TASK_QUEUE_NAME env var for explicit config 4. Dockerfile improvements: - Include shared libraries for proper binary execution The upstream app-recurring fix (PR #142) added: cfg.Server.TaskQueueName = cfg.TaskQueueName This ensures DCA server routes reshare tasks to dca_plugin_queue, allowing DCA worker to participate in 4-party TSS reshare. E2E Test Result: - Plugin install: 4 parties, 6.2s, keyshares in both MinIO buckets - Swap: USDC→BTC signed and broadcasted successfully - TxHash: 0xbd2fd6264045989c2b50059b8ca832a2582fb95972e0099dfe80ad24603c3661 Co-Authored-By: Claude Opus 4.5 --- k8s/base/dca/server.yaml | 4 +++ k8s/overlays/production/kustomization.yaml | 30 +++++++++++++++++++--- local/Dockerfile | 30 ++++++++++++++-------- local/cmd/vcli/cmd/policy_generate.go | 10 +++++--- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/k8s/base/dca/server.yaml b/k8s/base/dca/server.yaml index e2b0674..f5ef060 100644 --- a/k8s/base/dca/server.yaml +++ b/k8s/base/dca/server.yaml @@ -41,6 +41,8 @@ spec: value: "8088" - name: TASK_QUEUE_NAME value: "dca_plugin_queue" + - name: SERVER_TASK_QUEUE_NAME + value: "dca_plugin_queue" - name: SERVER_HOST value: "0.0.0.0" - name: SERVER_PORT @@ -157,6 +159,8 @@ spec: value: "8089" - name: TASK_QUEUE_NAME value: "dca_plugin_queue" + - name: SERVER_TASK_QUEUE_NAME + value: "dca_plugin_queue" - name: SERVER_HOST value: "0.0.0.0" - name: SERVER_PORT diff --git a/k8s/overlays/production/kustomization.yaml b/k8s/overlays/production/kustomization.yaml index 9e3a023..fc98f55 100644 --- a/k8s/overlays/production/kustomization.yaml +++ b/k8s/overlays/production/kustomization.yaml @@ -17,10 +17,10 @@ patchesStrategicMerge: # Image configuration for production # Override base images with proper registry and tags images: - # DCA Plugin (app-recurring) v1.0.82 + # DCA Plugin (app-recurring) - server v1.0.84 has TaskQueueName fix - name: docker.io/library/app-recurring-server:local-amd64 newName: ghcr.io/vultisig/app-recurring/server - newTag: v1.0.82 + newTag: v1.0.84 - name: docker.io/library/app-recurring-worker:local-amd64 newName: ghcr.io/vultisig/app-recurring/worker newTag: v1.0.82 @@ -40,10 +40,10 @@ images: - name: docker.io/library/vultisig-tx-indexer:local-amd64 newName: ghcr.io/vultisig/verifier/tx_indexer newTag: v0.1.16 - # VCLI v1.0.1 - fix: GetPluginServerURL reads env vars before defaults + # VCLI v1.0.3 - fix: billing fetch + shared libraries included in image - name: docker.io/library/vcli:local-amd64 newName: ghcr.io/vultisig/vcli - newTag: v1.0.1 + newTag: v1.0.3 # Patch deployments for production GHCR images patches: @@ -104,3 +104,25 @@ patches: kind: Deployment name: tx-indexer namespace: verifier + # Remove nodeSelector constraints from infra (allows any region) + - patch: |- + - op: remove + path: /spec/template/spec/nodeSelector + target: + kind: StatefulSet + name: postgres + namespace: infra + - patch: |- + - op: remove + path: /spec/template/spec/nodeSelector + target: + kind: StatefulSet + name: redis + namespace: infra + - patch: |- + - op: remove + path: /spec/template/spec/nodeSelector + target: + kind: StatefulSet + name: minio + namespace: infra diff --git a/local/Dockerfile b/local/Dockerfile index 6001131..738d056 100644 --- a/local/Dockerfile +++ b/local/Dockerfile @@ -1,16 +1,17 @@ -# Build stage -FROM golang:1.23-alpine AS builder +# Build stage - use Debian for glibc compatibility with go-wrappers +FROM golang:1.25-bookworm AS builder -RUN apk add --no-cache git +ARG TARGETARCH=amd64 + +RUN apt-get update && apt-get install -y git build-essential && rm -rf /var/lib/apt/lists/* WORKDIR /build # Copy go mod files COPY go.mod go.sum ./ -# Remove local replace directives for Docker build (use remote versions) -RUN sed -i '/^replace/,/^)/d' go.mod && \ - sed -i '/replace.*=>.*\.\.\//d' go.mod +# Remove only local path replace directives (keep version fixes) +RUN sed -i '/=> *\.\./d' go.mod # Download dependencies RUN go mod download @@ -18,16 +19,23 @@ RUN go mod download # Copy source code COPY cmd/ cmd/ -# Build binary -RUN CGO_ENABLED=0 GOOS=linux go build -o vcli ./cmd/vcli +# Build binary with CGO enabled for crypto libs +RUN CGO_ENABLED=1 GOOS=linux GOARCH=${TARGETARCH} go build -o vcli ./cmd/vcli + +# Find and copy shared libraries (from includes/linux/ subdirectory) +RUN mkdir -p /build/libs && \ + find /go/pkg/mod/github.com/vultisig/go-wrappers*/includes/linux -name "*.so" -exec cp {} /build/libs/ \; -# Runtime stage -FROM alpine:3.19 +# Runtime stage - use Debian slim for glibc compatibility +FROM debian:bookworm-slim -RUN apk add --no-cache ca-certificates +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* WORKDIR /app +# Copy shared libraries +COPY --from=builder /build/libs/*.so /usr/lib/ + # Copy binary from builder COPY --from=builder /build/vcli /usr/local/bin/vcli diff --git a/local/cmd/vcli/cmd/policy_generate.go b/local/cmd/vcli/cmd/policy_generate.go index 3737e6f..298e0d0 100644 --- a/local/cmd/vcli/cmd/policy_generate.go +++ b/local/cmd/vcli/cmd/policy_generate.go @@ -287,10 +287,14 @@ func fetchPluginBilling(pluginID string) ([]map[string]any, error) { var billing []map[string]any for _, p := range targetPricing { + frequency := "" + if p.Frequency != nil { + frequency = *p.Frequency + } entry := map[string]any{ - "type": p.Type, - "asset": p.Asset, - "amount": uint64(p.Amount), + "type": p.Type, + "frequency": frequency, + "amount": uint64(p.Amount), } billing = append(billing, entry) }