Skip to content

Commit 6c59b22

Browse files
authored
Merge pull request #563 from arkade-os/test/e2e-rotation-via-regtest-cli
test(e2e): drive signer rotation through the regtest CLI's set-signers
2 parents 0e7378b + 15eb9f1 commit 6c59b22

5 files changed

Lines changed: 51 additions & 78 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ on:
44
workflow_dispatch:
55
push:
66
branches: [master, next]
7+
# Run on all pull requests (not just those targeting master/next) so stacked
8+
# PRs onto feature branches get CI checks too.
79
pull_request:
8-
branches: [master, next]
910

1011
jobs:
1112
check:

‎packages/ts-sdk/test/e2e/compose.rotation.yml‎

Lines changed: 0 additions & 22 deletions
This file was deleted.

‎packages/ts-sdk/test/e2e/deprecatedSignerMigration.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,16 @@ describe("deprecated-signer migration (real rotation)", () => {
142142

143143
await rotateArkdSigner({ activeSignerPriv: B_SEC, deprecatedSigners: [deprecatedSpec] });
144144

145+
// arkd's restart drops the wallet's SSE subscription. Wait for it to
146+
// reconnect and re-track the deprecated signer's funds before returning,
147+
// so the it-block migrates on fresh state instead of racing the reconnect.
148+
// (The ContractWatcher reconnect fix resolves this in ~5s; if reconnect
149+
// regresses, this poll times out and the test fails deterministically.)
150+
await poll(async () => {
151+
const s = await vtxoManager.getDeprecatedSignerStatus();
152+
return s.some((x) => x.vtxoCount > 0 || x.boardingCount > 0) ? s : null;
153+
});
154+
145155
return { wallet, vtxoManager, contractManager, amount, stale, fromX, toX };
146156
};
147157

@@ -422,6 +432,14 @@ describe("deprecated-signer migration (real rotation)", () => {
422432

423433
const vtxoManager = await wallet.getVtxoManager();
424434

435+
// Wait for the wallet to re-track the boarding UTXO after arkd's
436+
// restart (its SSE subscription reconnects) before discovery /
437+
// migration, so this doesn't race the reconnect.
438+
await poll(async () => {
439+
const s = await vtxoManager.getDeprecatedSignerStatus();
440+
return s.some((x) => x.vtxoCount > 0 || x.boardingCount > 0) ? s : null;
441+
});
442+
425443
// Discovery (REAL): the stale boarding UTXO under A is DUE_NOW.
426444
const status = await vtxoManager.getDeprecatedSignerStatus();
427445
expect(status).toHaveLength(1);

‎packages/ts-sdk/test/e2e/utils.ts‎

Lines changed: 30 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,11 @@ function toXOnly(pubkeyHex: string): string {
351351
export type DeprecatedSignerSpec = string | { priv: string; cutoffDate?: number };
352352

353353
/**
354-
* Perform a real server-signer rotation: recreate `arkd-wallet` with the given
355-
* active signer (and optional deprecated signers, each with an optional cutoff
356-
* date), then restart `arkd` so it re-reads the signer set. Resolves once
357-
* `/v1/info` reflects the rotation.
354+
* Perform a real server-signer rotation by delegating to the regtest CLI's
355+
* `set-signers`: it recreates `arkd-wallet` with the given active signer (and
356+
* optional deprecated signers, each with an optional cutoff date), unlocks it,
357+
* and restarts `arkd` so it re-reads the signer set. Resolves once `/v1/info`
358+
* reflects the exact rotation.
358359
*
359360
* Keys are **private** keys (hex), matching the `ARKD_WALLET_SIGNER_KEY` /
360361
* `ARKD_WALLET_DEPRECATED_SIGNER_KEYS` fixture env. The fixture must hold the
@@ -371,57 +372,32 @@ export async function rotateArkdSigner(params: {
371372
typeof d === "string" ? { priv: d, cutoffDate: undefined as number | undefined } : d,
372373
);
373374

374-
// Build the recreate subprocess env exactly as `regtest.mjs` would: defaults
375-
// + the package `.env.regtest`, so `ARKD_WALLET_IMAGE` resolves to the
376-
// rotation-capable local image and ports match. A bare vitest process does
377-
// not have this env, and the compose default image has no rotation support.
378-
// `regtest` is symlinked into the package dir by scripts/regtest.sh, so this
379-
// path resolves at e2e runtime.
380-
const { loadEnv } = await import("../../regtest/lib/env.mjs");
381-
loadEnv("regtest", ".env.regtest");
382-
383-
// arkd parses each entry as `<hexkey>[:<unix-seconds cutoff>]`.
384-
const env = {
385-
...process.env,
386-
ARKD_WALLET_SIGNER_KEY: activeSignerPriv,
387-
ARKD_WALLET_DEPRECATED_SIGNER_KEYS: deprecated
388-
.map((d) => (d.cutoffDate != null ? `${d.priv}:${d.cutoffDate}` : d.priv))
389-
.join(","),
390-
};
391-
392-
// Recreate ONLY arkd-wallet, reusing the named `ark_wallet_datadir` volume
393-
// so the on-chain wallet seed/state survive (never pass --volumes). The
394-
// three-file, profiled, project-named form matches the running stack
395-
// (regtest/lib/compose.mjs); the Go reference's single-file/profile-less
396-
// form would target the wrong compose project. Both `base` and `ark`
397-
// profiles must be enabled: `--no-deps` keeps arkd-wallet's dependencies
398-
// (bitcoin, nbxplorer — both `base`-gated) from being recreated, but they
399-
// must still be DEFINED in the project or compose rejects arkd-wallet's
400-
// `depends_on` as an "undefined service".
401-
execSync(
402-
[
403-
"docker compose",
404-
"-p arkade-regtest",
405-
"-f regtest/docker/compose.base.yml",
406-
"-f regtest/docker/compose.ark.yml",
407-
"-f test/e2e/compose.rotation.yml",
408-
"--profile base --profile ark",
409-
"up -d --force-recreate --no-deps arkd-wallet",
410-
].join(" "),
411-
{ stdio: "pipe", env },
412-
);
375+
// Delegate the rotation to the regtest CLI's `set-signers`, which owns the
376+
// recreate → unlock → restart-arkd → readiness mechanism (and the env/compose
377+
// plumbing). `regtest` is symlinked into the package dir by scripts/regtest.sh;
378+
// `--env .env.regtest` makes the CLI use THIS package's regtest env (image,
379+
// ports) — the same override the stack was started with — so the recreated
380+
// arkd-wallet matches the running stack. arkd parses each deprecated entry as
381+
// `<hexkey>[:<unix-seconds cutoff>]`.
382+
const deprecatedArg = deprecated
383+
.map((d) => (d.cutoffDate != null ? `${d.priv}:${d.cutoffDate}` : d.priv))
384+
.join(",");
385+
try {
386+
execSync(
387+
`node regtest/regtest.mjs set-signers --env .env.regtest --active ${activeSignerPriv}` +
388+
(deprecatedArg ? ` --deprecated ${deprecatedArg}` : ""),
389+
{ stdio: "pipe" },
390+
);
391+
} catch (err) {
392+
const e = err as { stderr?: Buffer; stdout?: Buffer; message?: string };
393+
throw new Error(
394+
`rotateArkdSigner: set-signers failed: ${e.stderr?.toString() || e.stdout?.toString() || e.message}`,
395+
);
396+
}
413397

414-
// arkd caches the signer pubkey set at startup, so it must restart to pick
415-
// up the rotated wallet. On this stack arkd auto-unlocks the recreated
416-
// wallet via its env unlocker (ARKD_UNLOCKER_TYPE=env) on restart, so no
417-
// admin unlock call is needed (unlike the Go reference's stack).
418-
execSync("docker container stop arkd", { stdio: "pipe" });
419-
await new Promise((r) => setTimeout(r, 5000));
420-
execSync("docker container start arkd", { stdio: "pipe", env });
421-
422-
// Wait for arkd readiness AND for the rotation to be observable: RPCs racing
423-
// the restart get "server not ready", so poll until `/v1/info` both succeeds
424-
// and reports the new active signer + the full deprecated set.
398+
// The CLI already waited for arkd to re-sync and verified the deprecated
399+
// COUNT; assert the EXACT pubkeys here (identity, not just count) and return
400+
// the resulting info. Poll briefly in case `/v1/info` is still settling.
425401
const expectedActive = toXOnly(
426402
hex.encode(await SingleKey.fromHex(activeSignerPriv).xOnlyPublicKey()),
427403
);

0 commit comments

Comments
 (0)