Skip to content

Commit

Permalink
✨ (signer-btc): Handle Musig psbt signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Jan 14, 2025
1 parent 118746b commit 286dfae
Show file tree
Hide file tree
Showing 7 changed files with 484 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
} from "@ledgerhq/device-management-kit";

import { type Psbt } from "@api/model/Psbt";
import { type PsbtSignature } from "@api/model/Signature";
import { type Wallet as ApiWallet } from "@api/model/Wallet";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import { type BuildPsbtTaskResult } from "@internal/app-binder/task/BuildPsbtTask";
import { type PsbtSignature } from "@internal/app-binder/task/SignPsbtTask";
import { type DataStoreService } from "@internal/data-store/service/DataStoreService";
import { type PsbtMapper } from "@internal/psbt/service/psbt/PsbtMapper";
import { type ValueParser } from "@internal/psbt/service/value/ValueParser";
Expand Down
32 changes: 32 additions & 0 deletions packages/signer/signer-btc/src/api/model/Signature.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
import { type HexaString } from "@ledgerhq/device-management-kit";

export type Signature = { r: HexaString; s: HexaString; v: number };

export type PartialSignature = {
inputIndex: number;
pubkey: Uint8Array;
signature: Uint8Array;
tapleafHash?: Uint8Array;
};

export type MusigPubNonce = {
inputIndex: number;
participantPubkey: Uint8Array;
aggregatedPubkey: Uint8Array;
tapleafHash?: Uint8Array;
pubnonce: Uint8Array;
};

export type MusigPartialSignature = {
inputIndex: number;
participantPubkey: Uint8Array;
aggregatedPubkey: Uint8Array;
tapleafHash?: Uint8Array;
partialSignature: Uint8Array;
};

export type PsbtSignature =
| PartialSignature
| MusigPartialSignature
| MusigPubNonce;

export const isPartialSignature = (
psbtSignature: PsbtSignature,
): psbtSignature is PartialSignature => "pubbkey" in psbtSignature;
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,10 @@ describe("SignMessageDeviceAction", () => {
deviceAction,
expectedStates,
makeDeviceActionInternalApiMock(),
done,
);

// Verify mocks calls parameters
// @todo Put this in a onDone handle of testDeviceActionStates
observable.subscribe({
complete: () => {
expect(signPersonalMessageMock).toHaveBeenCalledWith(
Expand All @@ -115,7 +116,6 @@ describe("SignMessageDeviceAction", () => {
},
}),
);
done();
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("SignPsbtDeviceAction", () => {
data: [
{
inputIndex: 0,
pubKeyAugmented: Uint8Array.from([0x04, 0x05, 0x06]),
pubkey: Uint8Array.from([0x04, 0x05, 0x06]),
signature: Uint8Array.from([0x01, 0x02, 0x03]),
},
],
Expand Down Expand Up @@ -125,7 +125,7 @@ describe("SignPsbtDeviceAction", () => {
output: [
{
inputIndex: 0,
pubKeyAugmented: Uint8Array.from([0x04, 0x05, 0x06]),
pubkey: Uint8Array.from([0x04, 0x05, 0x06]),
signature: Uint8Array.from([0x01, 0x02, 0x03]),
},
],
Expand All @@ -137,9 +137,10 @@ describe("SignPsbtDeviceAction", () => {
deviceAction,
expectedStates,
makeDeviceActionInternalApiMock(),
done,
);

// Verify mocks calls parameters
// @todo Put this in a onDone handle of testDeviceActionStates
observable.subscribe({
complete: () => {
expect(prepareWalletPolicyMock).toHaveBeenCalledWith(
Expand Down Expand Up @@ -167,7 +168,6 @@ describe("SignPsbtDeviceAction", () => {
},
}),
);
done();
},
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ import {
type SignPsbtDAOutput,
} from "@api/app-binder/SignPsbtDeviceActionTypes";
import { type Psbt as ApiPsbt } from "@api/model/Psbt";
import { type PsbtSignature } from "@api/model/Signature";
import { type Wallet as ApiWallet } from "@api/model/Wallet";
import { type BtcErrorCodes } from "@internal/app-binder/command/utils/bitcoinAppErrors";
import {
BuildPsbtTask,
type BuildPsbtTaskResult,
} from "@internal/app-binder/task/BuildPsbtTask";
import { PrepareWalletPolicyTask } from "@internal/app-binder/task/PrepareWalletPolicyTask";
import {
type PsbtSignature,
SignPsbtTask,
} from "@internal/app-binder/task/SignPsbtTask";
import { SignPsbtTask } from "@internal/app-binder/task/SignPsbtTask";
import type { DataStoreService } from "@internal/data-store/service/DataStoreService";
import type { PsbtMapper } from "@internal/psbt/service/psbt/PsbtMapper";
import type { ValueParser } from "@internal/psbt/service/value/ValueParser";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { type ValueParser } from "@internal/psbt/service/value/ValueParser";
import { type Wallet } from "@internal/wallet/model/Wallet";
import { type WalletSerializer } from "@internal/wallet/service/WalletSerializer";

const SIGN_PSBT_YIELD_RESULT = Uint8Array.from([
const SIGN_PSBT_YIELD_PARIAL_SIG_RESULT = Uint8Array.from([
0x00, 0x20, 0xf1, 0xe8, 0x42, 0x44, 0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7, 0xa8,
0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19, 0xa9, 0x3a, 0xb9, 0x6c, 0xc1, 0xee, 0xee,
0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38, 0x5f, 0x5f, 0x12, 0x4d, 0x63, 0x5c, 0xf2,
Expand All @@ -26,9 +26,40 @@ const SIGN_PSBT_YIELD_RESULT = Uint8Array.from([
0xd9, 0xfe, 0xb7, 0x9e, 0x25, 0x3b, 0xd2,
]);

const SIGN_PSBT_YIELD_MUSIG_PARIAL_SIG_RESULT = Uint8Array.from([
0xff, 0xfe, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x42, 0x44,
0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7, 0xa8, 0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19,
0xa9, 0x3a, 0xb9, 0x6c, 0xc1, 0xee, 0xee, 0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38,
0x5f, 0x5f, 0x12, 0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0, 0x16, 0xfc, 0x9d,
0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01, 0x66, 0x3c, 0x59, 0x59, 0x41, 0x13,
0xe5, 0x71, 0x00, 0x06, 0x3d, 0x9d, 0xcc, 0xd7, 0x8f, 0xb3, 0x93, 0x82, 0xdb,
0xf8, 0x0a, 0x8f, 0x11, 0x50, 0xfd, 0x59, 0xd9, 0xfe, 0xb7, 0x9e, 0x25, 0x3b,
0xd2, 0xfe, 0xee, 0x33, 0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b,
0xe2, 0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0, 0x16, 0xfc,
0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01, 0x66, 0x3c,
]);

const SIGN_PSBT_YIELD_MUSIG_PUB_NONCE_RESULT = Uint8Array.from([
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x42, 0x44,
0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7, 0xa8, 0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19,
0xa9, 0x3a, 0xb9, 0x6c, 0xc1, 0xee, 0xee, 0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38,
0x5f, 0x5f, 0x12, 0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0, 0x16, 0xfc, 0x9d,
0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01, 0x66, 0x3c, 0x59, 0x01, 0x4d, 0x63,
0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2, 0x77, 0x71, 0x2e, 0xad, 0x07,
0xb4, 0x48, 0x96, 0xdf, 0xb0, 0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd,
0x9a, 0x01, 0x66, 0x3c, 0x59, 0x59, 0x41, 0x13, 0xe5, 0x71, 0x00, 0x06, 0x3d,
0x9d, 0xcc, 0xd7, 0x8f, 0xb3, 0x93, 0x82, 0xdb, 0xf8, 0x0a, 0x8f, 0x11, 0x50,
0xfd, 0x59, 0xd9, 0xfe, 0xb7, 0x9e, 0x25, 0x3b, 0xd2, 0xfe, 0xee, 0x33, 0x4d,
0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2, 0x77, 0x71, 0x2e, 0xad,
0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0, 0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22,
0xbd, 0x9a, 0x01, 0x66, 0x3c,
]);

describe("SignPsbtTask", () => {
describe("run", () => {
it("should return signatures", async () => {
it("should return partial signatures", async () => {
// given
const api = {
sendCommand: jest.fn(),
Expand Down Expand Up @@ -58,7 +89,7 @@ describe("SignPsbtTask", () => {
data: [],
}),
),
getYieldedResults: () => [SIGN_PSBT_YIELD_RESULT],
getYieldedResults: () => [SIGN_PSBT_YIELD_PARIAL_SIG_RESULT],
}) as unknown as ContinueTask;

// when
Expand Down Expand Up @@ -91,7 +122,7 @@ describe("SignPsbtTask", () => {
data: [
{
inputIndex: 0,
pubKeyAugmented: Uint8Array.from([
pubkey: Uint8Array.from([
0xf1, 0xe8, 0x42, 0x44, 0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7,
0xa8, 0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19, 0xa9, 0x3a, 0xb9,
0x6c, 0xc1, 0xee, 0xee, 0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38,
Expand All @@ -111,6 +142,193 @@ describe("SignPsbtTask", () => {
}),
);
});
it("should return musig partial signatures", async () => {
// given
const api = {
sendCommand: jest.fn(),
} as unknown as InternalApi;
const psbt = {
getGlobalValue: () => Maybe.of(Uint8Array.from([0x03])),
} as unknown as Psbt;
const wallet = {
hmac: Uint8Array.from([0x04]),
} as Wallet;
const psbtCommitment = {
globalCommitment: Uint8Array.from([0x03]),
inputsRoot: Uint8Array.from([0x01]),
outputsRoot: Uint8Array.from([0x02]),
} as PsbtCommitment;
const dataStore = {} as DataStore;
const walletSerializer = {
getId: jest.fn(() => Uint8Array.from([0x05])),
} as unknown as WalletSerializer;
const valueParser = {
getVarint: jest.fn(() => Maybe.of(42)),
} as unknown as ValueParser;
const continueTaskFactory = () =>
({
run: jest.fn().mockResolvedValue(
CommandResultFactory({
data: [],
}),
),
getYieldedResults: () => [SIGN_PSBT_YIELD_MUSIG_PARIAL_SIG_RESULT],
}) as unknown as ContinueTask;

// when
const signatures = await new SignPsbtTask(
api,
{
psbt,
wallet,
psbtCommitment,
dataStore,
},
walletSerializer,
valueParser,
continueTaskFactory,
).run();
// then
expect(api.sendCommand).toHaveBeenCalledWith(
new SignPsbtCommand({
globalCommitment: Uint8Array.from([0x03]),
inputsCount: 42,
inputsRoot: Uint8Array.from([0x01]),
outputsCount: 42,
outputsRoot: Uint8Array.from([0x02]),
walletId: Uint8Array.from([0x05]),
walletHmac: Uint8Array.from([0x04]),
}),
);
expect(signatures).toStrictEqual(
CommandResultFactory({
data: [
{
inputIndex: 0,
partialSignature: Uint8Array.from([
0xe8, 0x42, 0x44, 0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7, 0xa8,
0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19, 0xa9, 0x3a, 0xb9, 0x6c,
0xc1, 0xee, 0xee, 0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38, 0x5f,
0x5f, 0x12,
]),
participantPubkey: Uint8Array.from([
0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0,
0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01,
0x66, 0x3c, 0x59,
]),
aggregatedPubkey: Uint8Array.from([
0x59, 0x41, 0x13, 0xe5, 0x71, 0x00, 0x06, 0x3d, 0x9d, 0xcc,
0xd7, 0x8f, 0xb3, 0x93, 0x82, 0xdb, 0xf8, 0x0a, 0x8f, 0x11,
0x50, 0xfd, 0x59, 0xd9, 0xfe, 0xb7, 0x9e, 0x25, 0x3b, 0xd2,
0xfe, 0xee, 0x33,
]),
tapleafHash: Uint8Array.from([
0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0,
0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01,
0x66, 0x3c,
]),
},
],
}),
);
});
it("should return musig pub nonce", async () => {
// given
const api = {
sendCommand: jest.fn(),
} as unknown as InternalApi;
const psbt = {
getGlobalValue: () => Maybe.of(Uint8Array.from([0x03])),
} as unknown as Psbt;
const wallet = {
hmac: Uint8Array.from([0x04]),
} as Wallet;
const psbtCommitment = {
globalCommitment: Uint8Array.from([0x03]),
inputsRoot: Uint8Array.from([0x01]),
outputsRoot: Uint8Array.from([0x02]),
} as PsbtCommitment;
const dataStore = {} as DataStore;
const walletSerializer = {
getId: jest.fn(() => Uint8Array.from([0x05])),
} as unknown as WalletSerializer;
const valueParser = {
getVarint: jest.fn(() => Maybe.of(42)),
} as unknown as ValueParser;
const continueTaskFactory = () =>
({
run: jest.fn().mockResolvedValue(
CommandResultFactory({
data: [],
}),
),
getYieldedResults: () => [SIGN_PSBT_YIELD_MUSIG_PUB_NONCE_RESULT],
}) as unknown as ContinueTask;

// when
const signatures = await new SignPsbtTask(
api,
{
psbt,
wallet,
psbtCommitment,
dataStore,
},
walletSerializer,
valueParser,
continueTaskFactory,
).run();
// then
expect(api.sendCommand).toHaveBeenCalledWith(
new SignPsbtCommand({
globalCommitment: Uint8Array.from([0x03]),
inputsCount: 42,
inputsRoot: Uint8Array.from([0x01]),
outputsCount: 42,
outputsRoot: Uint8Array.from([0x02]),
walletId: Uint8Array.from([0x05]),
walletHmac: Uint8Array.from([0x04]),
}),
);
expect(signatures).toStrictEqual(
CommandResultFactory({
data: [
{
inputIndex: 0,
pubnonce: Uint8Array.from([
0xe8, 0x42, 0x44, 0x7f, 0xae, 0x7b, 0x1c, 0x6e, 0xb7, 0xa8,
0xa7, 0x85, 0xf7, 0x76, 0xfa, 0x19, 0xa9, 0x3a, 0xb9, 0x6c,
0xc1, 0xee, 0xee, 0xe9, 0x47, 0xc1, 0x71, 0x13, 0x38, 0x5f,
0x5f, 0x12, 0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6,
0x7b, 0xe2, 0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96,
0xdf, 0xb0, 0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd,
0x9a, 0x01, 0x66, 0x3c, 0x59, 0x01,
]),
participantPubkey: Uint8Array.from([
0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0,
0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01,
0x66, 0x3c, 0x59,
]),
aggregatedPubkey: Uint8Array.from([
0x59, 0x41, 0x13, 0xe5, 0x71, 0x00, 0x06, 0x3d, 0x9d, 0xcc,
0xd7, 0x8f, 0xb3, 0x93, 0x82, 0xdb, 0xf8, 0x0a, 0x8f, 0x11,
0x50, 0xfd, 0x59, 0xd9, 0xfe, 0xb7, 0x9e, 0x25, 0x3b, 0xd2,
0xfe, 0xee, 0x33,
]),
tapleafHash: Uint8Array.from([
0x4d, 0x63, 0x5c, 0xf2, 0x52, 0xae, 0x26, 0xa6, 0x7b, 0xe2,
0x77, 0x71, 0x2e, 0xad, 0x07, 0xb4, 0x48, 0x96, 0xdf, 0xb0,
0x16, 0xfc, 0x9d, 0x03, 0xa3, 0xe9, 0x22, 0xbd, 0x9a, 0x01,
0x66, 0x3c,
]),
},
],
}),
);
});
});
describe("errors", () => {
it("should return an error if continue task fails", async () => {
Expand Down
Loading

0 comments on commit 286dfae

Please sign in to comment.