Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88dadb3
ark-lib: add txsigner shared tapscript-signing primitive
Kukks Jun 19, 2026
723cdfe
arkd-signer: add module, config, and key-injected signer app
Kukks Jun 19, 2026
53c1193
arkd-signer: add gRPC SignerService handler, server, and health/telem…
Kukks Jun 19, 2026
3c83f02
arkd-signer: add cmd entrypoint; drop otel telemetry and pin deps to …
Kukks Jun 19, 2026
6eb62e8
arkd-signer: add deprecated signer keys (config, per-leaf selection, …
Kukks Jun 19, 2026
757b5d6
arkd-wallet: remove embedded SignerService and operator key (moved to…
Kukks Jun 19, 2026
56d51c4
arkd: drop in-wallet signer key injection; require ARKD_SIGNER_ADDR
Kukks Jun 19, 2026
42361b2
build: add arkd-signer image, build script, Makefile target, compose …
Kukks Jun 19, 2026
c0b8c08
arkd: signer load is url-only (drop --signer-prvkey); document arkd-s…
Kukks Jun 19, 2026
91f0c7b
arkd: sign forfeit connector input via wallet in fraud reaction
Kukks Jun 19, 2026
a91b876
deps: bump x/crypto v0.52.0 + x/net v0.55.0 (security); exclude h2c S…
Kukks Jun 23, 2026
c75709c
arkd-signer: redact config secrets; tidy signer-split docs, dev scrip…
bitcoin-coder-bob Jun 26, 2026
e3bcbb3
arkd-signer: address review (secret-key length, input-index bounds, C…
bitcoin-coder-bob Jun 26, 2026
1f54bc7
Merge remote-tracking branch 'origin/master' into arkd-signer
bitcoin-coder-bob Jul 16, 2026
53856a2
pkg/arkd-signer: bump go directive to 1.26.5
bitcoin-coder-bob Jul 16, 2026
3b5c908
arkd-signer: bump arkdsigner.Dockerfile builder to go 1.26.5
bitcoin-coder-bob Jul 17, 2026
059f353
txsigner: dedupe finalization on script.FinalizeVtxoScript
bitcoin-coder-bob Jul 21, 2026
14e27c1
Merge remote-tracking branch 'origin/master' into arkd-signer
bitcoin-coder-bob Jul 23, 2026
524e3b6
arkd-signer: bump grpc to v1.82.1 to clear GHSA-hrxh-6v49-42gf
bitcoin-coder-bob Jul 23, 2026
d56b8fa
arkd-signer: reject invalid signing scalars, cover every closure type
bitcoin-coder-bob Jul 30, 2026
644e309
arkd-signer: report real readiness, bind before Start returns, unpubl…
bitcoin-coder-bob Jul 30, 2026
c572c47
arkd-signer: gate arkd on signer health, make Watch stream status
bitcoin-coder-bob Jul 30, 2026
7e1d60f
Merge remote-tracking branch 'origin/master' into arkd-signer
bitcoin-coder-bob Jul 30, 2026
0513a9a
deps: align pkg/arkd-signer golang.org/x deps with the root module
bitcoin-coder-bob Jul 30, 2026
2d3754d
arkd-signer: drop CORS wildcard, cover the signing handler and forfei…
bitcoin-coder-bob Jul 30, 2026
691cb5c
test: read forfeit partial sigs before finalizing
bitcoin-coder-bob Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ version: "2"
run:
timeout: 5m

linters:
exclusions:
rules:
# h2c.NewHandler is deprecated in golang.org/x/net v0.55.0 but is still
# required for unencrypted HTTP/2 serving; migrating to http.Server's
# Protocols field is tracked as a separate follow-up.
- path: interface/grpc/service\.go
linters:
- staticcheck
text: SA1019

formatters:
enable:
- gofmt
Expand Down
29 changes: 13 additions & 16 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
.PHONY: \
build build-all build-cli build-wallet clean cov \
build build-all build-cli build-signer build-wallet clean cov \
docker-run docker-run-light docker-stop droppg droppgtest \
help integrationtest lint migrate pg pgmigrate pgsqlc pgtest \
pprof proto proto-lint psql \
redis-down redis-test-down redis-test-up redis-up \
run run-light run-signer run-simulation run-wallet run-wallet-nosigner \
run run-light run-signer run-simulation run-wallet \
sqlc test test-pkg vet

define setup_env
Expand Down Expand Up @@ -42,6 +42,11 @@ build-wallet:
@echo "Building arkd wallet binary..."
@bash ./scripts/build-arkd-wallet

## build-signer: build arkd signer for your platforms
build-signer:
@echo "Building arkd signer binary..."
@bash ./scripts/build-arkd-signer

## clean: run go clean
clean:
@echo "Cleaning..."
Expand Down Expand Up @@ -173,12 +178,11 @@ run-light: clean
$(call setup_env, envs/arkd.light.env)
@go run ./cmd/arkd

## run-signer: run arkd wallet as signer without a wallet
## run-signer: run arkd-signer in dev mode
run-signer:
@echo "Running signer in dev mode"
@docker compose -f docker-compose.regtest.yml up -d pg nbxplorer
@echo "Running arkd-signer in dev mode"
$(call setup_env, envs/signer.dev.env)
@go run ./cmd/arkd-wallet
@go run ./cmd/arkd-signer

## run-simulation: run the multi-VTXO batch settlement test
## Usage: make run-simulation [CLIENTS=n]
Expand All @@ -205,7 +209,7 @@ run-simulation:
## run-vtxo-chain-simulation: run the vtxo chain smoke test
## Usage: make run-vtxo-chain-simulation [CHAIN_LENGTH=n]
# Examples:
# make run-vtxo-chain-simulation # Default: 50 chained offchain txs
# make run-vtxo-chain-simulation # Default: 50 chained offchain txs
# make run-vtxo-chain-simulation CHAIN_LENGTH=100 # 100 chained offchain txs
run-vtxo-chain-simulation:
@echo "Stopping any existing Docker environment..."
Expand All @@ -221,18 +225,11 @@ run-vtxo-chain-simulation:
'
@echo "Test completed."

## run-wallet: run arkd wallet based on nbxplorer in dev mode on regtest with a pre-loaded signer private key
## run-wallet: run arkd wallet based on nbxplorer in dev mode on regtest
run-wallet:
@echo "Running arkd wallet in dev mode with NBXplorer on regtest with pre-loaded signer private key..."
@docker compose -f docker-compose.regtest.yml up -d pg nbxplorer
$(call setup_env, envs/arkd-wallet.regtest.env)
@go run ./cmd/arkd-wallet

## run-wallet-nosigner: run arkd wallet based on nbxplorer in dev mode on regtest without a pre-loaded signer private key
run-wallet-nosigner:
@echo "Running arkd wallet in dev mode with NBXplorer on regtest..."
@docker compose -f docker-compose.regtest.yml up -d pg nbxplorer
$(call setup_env, envs/arkd-wallet-nosigner.regtest.env)
$(call setup_env, envs/arkd-wallet.regtest.env)
@go run ./cmd/arkd-wallet

## sqlc: compile sql queries for sqlite
Expand Down
62 changes: 37 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The Operator's role is designed with strict boundaries that ensure users always
* mutinynet
* mainnet

and makes use of [arkd-wallet](./pkg/arkd-wallet/), an on-chain wallet based on NBXplorer, as liquidity provider and optionally also as signer.
and makes use of [arkd-wallet](./pkg/arkd-wallet/), an on-chain wallet based on NBXplorer, as liquidity provider, and [arkd-signer](./pkg/arkd-signer/) as the transaction signer.

## Usage Documentation

Expand Down Expand Up @@ -82,7 +82,7 @@ The `arkd` server can be configured using environment variables and the admin se
| `ARKD_REDIS_NUM_OF_RETRIES` | Maximum number of retries for Redis write operations in case of conflicts | - |
| `ARKD_ESPLORA_URL` | Esplora API URL | `https://blockstream.info/api` |
| `ARKD_WALLET_ADDR` | The arkd wallet address to connect to in the form `host:port` | - |
| `ARKD_SIGNER_ADDR` | The signer address to connect to in the form `host:port` | value of `ARKD_WALLET_ADDR` |
| `ARKD_SIGNER_ADDR` | The signer address to connect to in the form `host:port` (required) | - |
| `ARKD_NO_MACAROONS` | Disable macaroon authentication | `false` |
| `ARKD_NO_TLS` | Disable TLS | `true` |
| `ARKD_UNLOCKER_TYPE` | Wallet unlocker type (env, file) to enable auto-unlock | - |
Expand Down Expand Up @@ -163,13 +163,28 @@ export ARKD_WALLET_NBXPLORER_URL=http://localhost:32838

### Configure signer

`arkd-wallet` can be used also as signer.
`arkd-signer` is a standalone service that holds the operator signing key and signs `arkd`'s protocol transactions. It is configured via the following environment variables:

The configuration can be done either via env vars or via API. To enable `arkd-wallet`'s signer mode use this environment variable:
| Environment Variable | Description | Default |
|-------------------------------|-----------------------------------------------------------------------------------|-------------|
| `ARKD_SIGNER_SECRET_KEY` | Hex-encoded operator signing key (required, sensitive) | - |
| `ARKD_SIGNER_DEPRECATED_KEYS` | Comma-separated old keys still accepted for signing, for key rotation (sensitive) | - |
| `ARKD_SIGNER_PORT` | Port to listen on | `6061` |
| `ARKD_SIGNER_LOG_LEVEL` | Logging level (0-6, where 6 is trace) | `4` (info) |

Configure the signing key (required):

```sh
# Make sure to use a random private key, this is just an example.
export ARKD_WALLET_SIGNER_KEY=19422b10efd05403820ff6a3365422be2fc5f07f34a6d1603f7298328f0f80f6
export ARKD_SIGNER_SECRET_KEY=19422b10efd05403820ff6a3365422be2fc5f07f34a6d1603f7298328f0f80f6
```
Comment on lines 177 to +180

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid documenting a concrete signer private key value.

Publishing a realistic fixed secret encourages unsafe copy-paste and creates recurring secret-scan noise; prefer a placeholder and a generation command.

Suggested doc tweak
 # Make sure to use a random private key, this is just an example.
-export ARKD_SIGNER_SECRET_KEY=19422b10efd05403820ff6a3365422be2fc5f07f34a6d1603f7298328f0f80f6
+export ARKD_SIGNER_SECRET_KEY=<32-byte-hex-private-key>
+# Example generation:
+# export ARKD_SIGNER_SECRET_KEY=$(openssl rand -hex 32)
🧰 Tools
🪛 Betterleaks (1.5.0)

[high] 169-169: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 167 - 170, The README.md documentation shows a
concrete private key value for ARKD_SIGNER_SECRET_KEY which is a security risk
and encourages unsafe copy-paste behavior. Replace the actual key value with a
placeholder (like a series of zeros or a descriptive placeholder) and add a
clear command that shows users how to generate their own random private key
instead. This way the documentation provides guidance on how to create a proper
secret without exposing or promoting the use of a fixed hardcoded value.

Source: Linters/SAST tools


To rotate the signer key while still accepting coins locked to old keys, set `ARKD_SIGNER_DEPRECATED_KEYS` to a comma-separated list of old keys. Each entry is a hex key, optionally followed by `:` and a cutoff date, which is a Unix timestamp in **seconds** (UTC), after which clients should stop locking new funds to that key. Omit the cutoff to leave it unset:

```sh
# Format: <hexkey>[:<cutoff_unix_seconds>],<hexkey>[:<cutoff_unix_seconds>]
# Example with two deprecated keys: the first retires at a cutoff date, the second has no cutoff.
export ARKD_SIGNER_DEPRECATED_KEYS=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6:1735689600,f0e1d2c3b4a59687766554433221100ff1e2d3c4b5a69788796a5b4c3d2e1f00
```

### Connect to wallet
Expand All @@ -183,15 +198,11 @@ export ARKD_WALLET_ADDR=localhost:6060

### Connect to signer

By default, `arkd` makes use of the provided `arkd-wallet` also as signer, but you can customize its url either via environment variable or via API.

#### Connect to custom signer

To connect `arkd` to a custom signer use this environment variable:
`arkd` connects to `arkd-signer` via this environment variable (required):

```sh
# Make sure to use the right URL in the form host:port, this is just an example.
export ARKD_SIGNER_ADDR=localhost:7071
export ARKD_SIGNER_ADDR=localhost:6061
```

### Setup arkd
Expand All @@ -201,12 +212,17 @@ export ARKD_SIGNER_ADDR=localhost:7071
arkd-wallet
```

2. Start arkd:
2. Start the signer:
```sh
arkd-signer
```

3. Start arkd (requires `ARKD_WALLET_ADDR` and `ARKD_SIGNER_ADDR`):
```sh
arkd
```

3. Create a new wallet:
4. Create a new wallet:
```sh
arkd wallet create --password <password>
```
Expand All @@ -215,14 +231,11 @@ export ARKD_SIGNER_ADDR=localhost:7071
```sh
arkd wallet create --mnemonic "your twelve word mnemonic phrase here" --password <password>
```
4. Only if you didn't configure either the wallet as signer, or a custom signer, you must load the signer before unlocking the wallet, or `arkd` will fail to start:

You can repoint `arkd` at a different signer URL at runtime if needed:
```sh
# If you configured a custom signer
arkd signer load --signer-url localhost:7071
# Or, if you want to configure the wallet as signer with a private key
arkd signer load --signer-prvkey 19422b10efd05403820ff6a3365422be2fc5f07f34a6d1603f7298328f0f80f6
arkd signer load --signer-url localhost:6061
```
Remember, if you use this command, you must use it at every restart unless you export the required environment variable(s).

5. Unlock the wallet:
```sh
Expand Down Expand Up @@ -256,7 +269,8 @@ For a complete list of available commands and options:
- [`api-spec`](./api-spec/): Ark Protocol Buffer API specification.
- [`pkg`](./pkg/): collection of reusable packages and services.
- [`ark-lib`](./pkg/ark-lib): collection of data structures and functions reusable by arkd and sdk.
- [`arkd-wallet`](./pkg/arkd-wallet): bitcoin wallet service used as liquidity provider and signer.
- [`arkd-wallet`](./pkg/arkd-wallet): bitcoin wallet service used as liquidity provider.
- [`arkd-signer`](./pkg/arkd-signer): standalone service that holds the operator key and signs arkd's protocol transactions.
- [`ark-cli`](./pkg/ark-cli): ark offchain and onchain wallet as command line interface.
- [`internal`](./internal): arkd implementation.
- [`core`](./internal/core): contains the core business logic of arkd.
Expand Down Expand Up @@ -308,14 +322,10 @@ To compile the `arkd` binary from source, you can use the following Make command
go mod download
```

6. Run arkd wallet in dev mode:
6. Run arkd wallet and signer in dev mode (each in its own tab):

```sh
# run wallet with signer enabled
make run-wallet
# or, run wallet with signer disabled...
make run-wallet-nosigner
# ... and in another tab run a custom signer
make run-signer
```

Expand Down Expand Up @@ -352,6 +362,8 @@ To compile the `arkd` binary from source, you can use the following Make command
make docker-stop
```

`make docker-run` brings up the full stack defined in `docker-compose.regtest.yml` (postgres, redis, nbxplorer, `arkd-wallet`, `arkd-signer`, and `arkd`).

### Protobuf Breaking Change Detection

To ensure API compatibility, breaking changes in protobuf definitions are automatically detected:
Expand Down
31 changes: 31 additions & 0 deletions arkdsigner.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# First stage: build the arkd-signer binary
FROM golang:1.26.5 AS builder

ARG VERSION
ARG TARGETOS
ARG TARGETARCH

WORKDIR /app

COPY . .

RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="-X 'main.Version=${VERSION}'" -o /app/bin/arkd-signer ./cmd/arkd-signer/main.go

# Second stage: minimal runtime image
FROM alpine:3.20

RUN apk update && apk upgrade

WORKDIR /app

COPY --from=builder /app/bin/arkd-signer /app/

ENV PATH="/app:${PATH}"

# /healthz maps a NOT_SERVING health response to 503, and the signer reports
# NOT_SERVING until its key is usable, so this gates on readiness rather than on
# the process having started.
HEALTHCHECK --interval=5s --timeout=3s --start-period=5s --retries=5 \
CMD wget -q --spider "http://127.0.0.1:${ARKD_SIGNER_PORT:-6061}/healthz" || exit 1

ENTRYPOINT [ "arkd-signer" ]
Comment on lines +15 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Run the runtime container as a non-root user.

The signer holds sensitive key material; running as root increases blast radius if compromised.

Suggested hardening
 FROM alpine:3.20
 
 RUN apk update && apk upgrade
 
 WORKDIR /app
 
-COPY --from=builder /app/bin/arkd-signer /app/
+RUN addgroup -S signer && adduser -S -G signer signer
+COPY --from=builder --chown=signer:signer /app/bin/arkd-signer /app/
 
 ENV PATH="/app:${PATH}"
 
+USER signer
 ENTRYPOINT [ "arkd-signer" ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@arkdsigner.Dockerfile` around lines 15 - 25, The runtime container specified
by the FROM alpine:3.20 directive is currently running as root by default, which
poses a security risk for the sensitive key material in the arkd-signer binary.
Add a USER instruction to create a non-root user (such as 'appuser' or similar)
using a RUN command with apk add, then add a USER directive before the
ENTRYPOINT to switch to this non-root user so the arkd-signer process runs with
reduced privileges instead of as root.

44 changes: 44 additions & 0 deletions cmd/arkd-signer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"os"
"os/signal"
"syscall"

"github.com/arkade-os/arkd/pkg/arkd-signer/config"
grpcservice "github.com/arkade-os/arkd/pkg/arkd-signer/interface/grpc"
log "github.com/sirupsen/logrus"
)

func main() {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("invalid arkd-signer config: %s", err)
}

log.SetLevel(log.Level(cfg.LogLevel))

svc, err := grpcservice.NewService(cfg)
if err != nil {
log.Fatalf("failed to create arkd-signer service: %s", err)
}

log.Infof("arkd-signer config: %s", cfg)

log.Info("starting arkd-signer service...")
if err := svc.Start(); err != nil {
log.Fatalf("failed to start arkd-signer service: %s", err)
}
log.Infof("arkd-signer listens on: %v", cfg.Port)

log.RegisterExitHandler(svc.Stop)

sigChan := make(chan os.Signal, 1)
signal.Notify(
sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGHUP, os.Interrupt,
)
<-sigChan

log.Info("shutting down arkd-signer service...")
log.Exit(0)
}
12 changes: 6 additions & 6 deletions cmd/arkd-wallet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func main() {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("invalid config: %s", err)
log.Fatalf("invalid arkd-wallet config: %s", err)
}

log.SetLevel(log.Level(cfg.LogLevel))
Expand All @@ -24,16 +24,16 @@ func main() {

svc, err := grpcservice.NewService(cfg)
if err != nil {
log.Fatalf("failed to create service: %s", err)
log.Fatalf("failed to create arkd-wallet service: %s", err)
}

log.Infof("arkd wallet config: %+v", cfg)
log.Infof("arkd-wallet config: %+v", cfg)

log.Info("starting service...")
log.Info("starting arkd-wallet service...")
if err := svc.Start(); err != nil {
log.Fatalf("failed to start service: %s", err)
}
log.Infof("arkd wallet listens on: %v", cfg.Port)
log.Infof("arkd-wallet listens on: %v", cfg.Port)

log.RegisterExitHandler(svc.Stop)

Expand All @@ -43,6 +43,6 @@ func main() {
)
<-sigChan

log.Info("shutting down service...")
log.Info("shutting down arkd-wallet service...")
log.Exit(0)
}
15 changes: 4 additions & 11 deletions cmd/arkd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ var (
}
signerLoadCmd = &cli.Command{
Name: "load",
Usage: "Load the ark signer address or private key",
Usage: "Load the ark signer url",
Action: signerLoadAction,
Flags: []cli.Flag{signerKeyFlag, signerUrlFlag},
Flags: []cli.Flag{signerUrlFlag},
}
noteCmd = &cli.Command{
Name: "note",
Expand Down Expand Up @@ -447,13 +447,9 @@ func walletWithdrawAction(ctx *cli.Context) error {

func signerLoadAction(ctx *cli.Context) error {
baseURL := ctx.String(urlFlagName)
signerKey := ctx.String(signerKeyFlagName)
signerUrl := ctx.String(signerUrlFlagName)
if signerKey == "" && signerUrl == "" {
return fmt.Errorf("either private key or url must be provided")
}
if signerKey != "" && signerUrl != "" {
return fmt.Errorf("private key and url are mutually exclusive, only one must be provided")
if signerUrl == "" {
return fmt.Errorf("signer url must be provided")
}
macaroon, tlsConfig, err := getCredentials(ctx)
if err != nil {
Expand All @@ -462,9 +458,6 @@ func signerLoadAction(ctx *cli.Context) error {

url := fmt.Sprintf("%s/v1/admin/signer", baseURL)
body := fmt.Sprintf(`{"signerUrl": "%s"}`, signerUrl)
if signerKey != "" {
body = fmt.Sprintf(`{"signerPrivateKey": "%s"}`, signerKey)
}

if _, err := post[struct{}](url, body, "", macaroon, tlsConfig); err != nil {
return err
Expand Down
Loading
Loading