Skip to content

Commit 41c8ee0

Browse files
committed
fix(wallet): register boarding baseline contract under deprecated signers
Follow-up to d7b936c (which fanned the default/delegate baseline over deprecated signers): the index-0 boarding baseline was still registered only under the current signer, so a wallet loaded with boarding funds minted under a now-rotated signer never restored — hence never watched — that boarding contract at boot (only the boarding WATCH/history path saw the address). Fan the boarding baseline over current ∪ deprecated signers (same axis as the default/delegate matrix), deduped against the offchain matrix's scripts so a boarding script that coincides with an already-registered default/delegate one stays first-wins as that type (degenerate boardingExitDelay == a baseline timelock; sound servers keep them distinct). Extends the baseline test to require a boarding contract under the deprecated signer too, using realistic distinct boarding/unilateral exit delays (the shared mock equates them, which would mask the separate boarding row).
1 parent 6c86a52 commit 41c8ee0

2 files changed

Lines changed: 48 additions & 24 deletions

File tree

‎packages/ts-sdk/src/wallet/wallet.ts‎

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,22 +1516,38 @@ export class ReadonlyWallet implements IReadonlyWallet {
15161516
// the row exists.
15171517
const boardingCsvTimelock =
15181518
this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
1519-
const baselineBoarding = new DefaultVtxo.Script({
1520-
pubKey: baselinePubkey,
1521-
serverPubKey: this.boardingTapscript.options.serverPubKey,
1522-
csvTimelock: boardingCsvTimelock,
1523-
});
1524-
await ensureWalletContract(manager, {
1525-
type: "boarding",
1526-
params: {
1527-
pubKey: hex.encode(baselineBoarding.options.pubKey),
1528-
serverPubKey: hex.encode(baselineBoarding.options.serverPubKey),
1529-
csvTimelock: timelockToSequence(boardingCsvTimelock).toString(),
1530-
},
1531-
script: hex.encode(baselineBoarding.pkScript),
1532-
address: baselineBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
1533-
state: "active",
1534-
});
1519+
// Register the baseline boarding contract under the current signer AND
1520+
// every deprecated signer — the same signer axis as the default/delegate
1521+
// matrix above — so a wallet loaded with boarding funds minted under a
1522+
// now-rotated signer restores (and watches) that boarding contract at
1523+
// boot, not only via the boarding WATCH/history path. Boarding-exit CSV
1524+
// is a single server-wide delay, so only the signer axis fans here.
1525+
// Deduped against the offchain matrix's scripts (shared
1526+
// `seenBaselineScripts`): a boarding script that coincides with an
1527+
// already-registered default/delegate one stays first-wins as that type
1528+
// (degenerate boardingExitDelay == a baseline timelock; sound servers
1529+
// keep them distinct).
1530+
for (const serverPubKey of baselineSigners) {
1531+
const baselineBoarding = new DefaultVtxo.Script({
1532+
pubKey: baselinePubkey,
1533+
serverPubKey,
1534+
csvTimelock: boardingCsvTimelock,
1535+
});
1536+
const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
1537+
if (seenBaselineScripts.has(boardingScriptHex)) continue;
1538+
seenBaselineScripts.add(boardingScriptHex);
1539+
await ensureWalletContract(manager, {
1540+
type: "boarding",
1541+
params: {
1542+
pubKey: hex.encode(baselineBoarding.options.pubKey),
1543+
serverPubKey: hex.encode(serverPubKey),
1544+
csvTimelock: timelockToSequence(boardingCsvTimelock).toString(),
1545+
},
1546+
script: boardingScriptHex,
1547+
address: baselineBoarding.address(this.network.hrp, serverPubKey).encode(),
1548+
state: "active",
1549+
});
1550+
}
15351551

15361552
return manager;
15371553
}

‎packages/ts-sdk/test/serverSignerRotation.test.ts‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ describe("Offchain baseline matrix across server-signer rotation", () => {
286286
const PREV_SERVER = "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5";
287287
const PREV_SERVER_XONLY = PREV_SERVER.slice(2);
288288

289-
it("registers a baseline default contract under a deprecated signer at boot", async () => {
289+
it("registers baseline default + boarding contracts under a deprecated signer at boot", async () => {
290290
// Advertise PREV as deprecated so the wallet caches it at setup, before
291291
// the contract manager builds its index-0 baseline matrix.
292292
vi.stubGlobal(
@@ -297,6 +297,11 @@ describe("Offchain baseline matrix across server-signer rotation", () => {
297297
if (url.includes("/info"))
298298
return reply({
299299
...mockArkInfo,
300+
// Distinct from unilateralExitDelay (mockArkInfo: 144) so the
301+
// boarding baseline is a SEPARATE contract, not a first-wins
302+
// script collision with the default baseline. Production keeps
303+
// these delays distinct; the shared mock happens to equate them.
304+
boardingExitDelay: 288,
300305
deprecatedSigners: [{ pubkey: PREV_SERVER, cutoffDate: 9_999_999_999 }],
301306
});
302307
if (url.includes("subscribe") || url.includes("subscriptions"))
@@ -313,13 +318,16 @@ describe("Offchain baseline matrix across server-signer rotation", () => {
313318
).getContractManager();
314319

315320
const contracts = await contractRepository.getContracts({});
316-
const underPrev = contracts.filter(
317-
(c) => c.type === "default" && c.params.serverPubKey === PREV_SERVER_XONLY,
318-
);
319-
// The deprecated signer's baseline default contract must be registered
320-
// at boot so funds minted under the now-rotated signer are watched
321-
// without an explicit restore().
322-
expect(underPrev.length).toBeGreaterThan(0);
321+
const underPrev = (type: string) =>
322+
contracts.filter(
323+
(c) => c.type === type && c.params.serverPubKey === PREV_SERVER_XONLY,
324+
);
325+
// Both the default and boarding baseline contracts under the deprecated
326+
// signer must be registered at boot, so offchain VTXOs AND boarding
327+
// UTXOs minted under the now-rotated signer are watched without an
328+
// explicit restore().
329+
expect(underPrev("default").length).toBeGreaterThan(0);
330+
expect(underPrev("boarding").length).toBeGreaterThan(0);
323331
} finally {
324332
await wallet.dispose();
325333
}

0 commit comments

Comments
 (0)