Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ and PR conventions.
| Method | Path | Purpose |
| ------ | ----------------- | -------------------------------------------------- |
| `GET` | `/simpleWallet` | Pop a pre-generated 24-word wallet with 22 legacy addresses and their shielded pairs |
| `GET` | `/multisigWallet` | Generate N-of-M multisig wallets (`participants`, `numSignatures` query params) |
| `GET` | `/multisigWallet` | Generate N-of-M multisig wallets with their shielded pairs (`participants`, `numSignatures` query params) |
| `GET` | `/status` | Operator diagnostic: readiness, pool counts, genesis address, bootstrap phase, funding lifecycle |
| `GET` | `/ready` | Readiness probe — `200` when ready, `503` otherwise |
| `GET` | `/live` | Liveness probe — always `200` |
Expand Down Expand Up @@ -213,9 +213,9 @@ Lib-managed.

### `shieldedAddresses`

Alongside the legacy `addresses`, `/simpleWallet` returns one pre-calculated
shielded pair per BIP32 index (same `ADDRESS_COUNT` window, indices
`[0, ADDRESS_COUNT)`):
Alongside the legacy `addresses`, `/simpleWallet` — and every entry in
`/multisigWallet`'s `wallets` array — returns one pre-calculated shielded pair
per BIP32 index (same `ADDRESS_COUNT` window, indices `[0, ADDRESS_COUNT)`):

```jsonc
{
Expand Down Expand Up @@ -245,6 +245,13 @@ spend keys from `m/44'/280'/2'/0`, each derived per index with standard BIP32
`deriveChild`. A fixed-seed test pins index 0 against wallet-lib's committed
shielded fixture, so the two repos cannot drift apart unnoticed.

For `/multisigWallet` the pairs are **per participant**, unlike `addresses`:
the P2SH addresses are shared because they derive from the sorted pubkey set,
whereas shielded keys derive from each participant's own root key and the
redeem script plays no part. wallet-lib derives scan/spend keys whenever a
wallet starts from a root key, with no multisig exemption, so a multisig wallet
pays that cost at start whether or not the test touches a shielded feature.

The field is optional in the response type for consumers pinned to a wallet-lib
without shielded support; this service always populates it.

Expand Down
17 changes: 16 additions & 1 deletion __tests__/src/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,26 @@ describe("handleMultisigWallet happy path", () => {
);
expect(res.status).toBe(200);
const body = (await res.json()) as {
wallets: { addresses: string[]; multisigDebugData: { pubkeys: string[] } }[];
wallets: {
addresses: string[];
shieldedAddresses?: unknown[];
multisigDebugData: { pubkeys: string[] };
}[];
retrieveTimeMs: number;
};
expect(body.wallets).toHaveLength(2);
expect(body.wallets[0]!.addresses).toEqual(body.wallets[1]!.addresses);
// Assert at the wire, not just the service: `shieldedAddresses` is
// optional on MultisigWallet, so a handler projecting a subset of fields
// would drop it from the response with a green typecheck and a green
// service-layer suite. Serving the field IS the feature.
expect(body.wallets[0]!.shieldedAddresses).toHaveLength(
body.wallets[0]!.addresses.length,
);
// Shared P2SH addresses but per-participant shielded pairs.
expect(body.wallets[0]!.shieldedAddresses).not.toEqual(
body.wallets[1]!.shieldedAddresses,
);
expect(typeof body.retrieveTimeMs).toBe("number");
});
});
19 changes: 19 additions & 0 deletions __tests__/src/wallet-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,23 @@ describe("generateShieldedAddresses", () => {
// would hand callers shielded addresses this wallet cannot spend.
expect(w.shieldedAddresses).toEqual(generateShieldedAddresses(w.words));
});

test("generateMultisigWallet populates shieldedAddresses per participant", () => {
for (const w of generateMultisigWallet(3, 2)) {
expect(w.shieldedAddresses).toHaveLength(config.ADDRESS_COUNT);
for (const p of w.shieldedAddresses!) {
expect(Object.keys(p).sort()).toEqual(EXPECTED_KEYS);
}
expect(w.shieldedAddresses).toEqual(generateShieldedAddresses(w.words));
}
});

test("multisig shielded pairs are per-seed, not shared like addresses", () => {
const [a, b] = generateMultisigWallet(2, 2);
// The P2SH addresses are shared by design — they derive from the sorted
// pubkey set. Shielded pairs derive from each participant's own root key,
// so sharing them would hand a participant addresses it cannot spend.
expect(a!.addresses).toEqual(b!.addresses);
expect(a!.shieldedAddresses).not.toEqual(b!.shieldedAddresses);
});
});
9 changes: 7 additions & 2 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ export function handleSimpleWallet(_req: Request): Response {
*
* Validates the two query params (presence, integer, positivity,
* `numSignatures <= participants`) before delegating to the generator.
* No upper bound on `participants`: callers wanting an unusually large
* multisig pay the synchronous seed-generation cost themselves.
*
* The cost is not caller-local: this handler is synchronous, so seed
* generation and per-participant shielded derivation block the event loop
* for every other client — `/live`, `/ready` and `/fund` included. There is
* also an effective ceiling of 16 participants, above which P2SH derivation
* throws and surfaces as a generic 500. Both are tracked in
* HathorNetwork/hathor-integration-test-helper#32.
*/
export function handleMultisigWallet(req: Request): Response {
const start = nowMs();
Expand Down
7 changes: 7 additions & 0 deletions src/wallet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export interface MultisigDebugData {
export interface MultisigWallet {
words: string;
addresses: string[];
// Optional for the same reason as on SimpleWallet — consumers on older
// wallet-lib versions stay compatible; generateMultisigWallet always
// populates it. Derived from each participant's own seed: shielded keys
// come from the root key, and the P2SH redeem script plays no part, so
// these pairs are per-participant and not shared like `addresses`.
shieldedAddresses?: PrecalculatedShieldedAddress[];
multisigDebugData: MultisigDebugData;
}

Expand Down Expand Up @@ -171,6 +177,7 @@ export function generateMultisigWallet(
return allWords.map((words) => ({
words,
addresses: [...sharedAddresses],
shieldedAddresses: generateShieldedAddresses(words),
multisigDebugData: {
total: participants,
minSignatures: numSignatures,
Expand Down