Skip to content

Commit c40cd02

Browse files
OttoAllmendingerllm-git
andcommitted
feat(utxo-staking): force ECDSA signature for Babylon BTC staking
Implement BitGoStakingManager class that extends BabylonBtcStakingManager to use ECDSA signatures instead of BIP322. This supports compatibility with existing wallet functionality. Issue: BTC-2079 Co-authored-by: llm-git <[email protected]>
1 parent 8772ff5 commit c40cd02

File tree

2 files changed

+97
-10
lines changed

2 files changed

+97
-10
lines changed

modules/utxo-staking/src/babylon/stakingManager.ts

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,81 @@
11
import * as bitcoinjslib from 'bitcoinjs-lib';
22
import * as utxolib from '@bitgo/utxo-lib';
33
import * as vendor from '@bitgo/babylonlabs-io-btc-staking-ts';
4+
import {
5+
BIP322Sig,
6+
BTCSigType,
7+
ProofOfPossessionBTC,
8+
} from '@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop';
49

510
import { getStakingParams } from './stakingParams';
611

12+
/**
13+
* Subclass of BabylonBtcStakingManager with the sole purpose of forcing
14+
* a ECDSA signature.
15+
*/
16+
class BitGoStakingManager extends vendor.BabylonBtcStakingManager {
17+
constructor(
18+
network: bitcoinjslib.Network,
19+
stakingParams: vendor.VersionedStakingParams[],
20+
btcProvider: vendor.BtcProvider,
21+
babylonProvider: vendor.BabylonProvider
22+
) {
23+
super(network, stakingParams, btcProvider, babylonProvider);
24+
}
25+
26+
/**
27+
* Creates a proof of possession for the staker based on ECDSA signature.
28+
*
29+
* This is a parameterized version of the superclass method which infers
30+
* the signature type from the stakerBtcAddress.
31+
*
32+
* @param bech32Address - The staker's bech32 address on the babylon network.
33+
* @param stakerBtcAddress - The staker's BTC address.
34+
* @param sigType - The signature type (BIP322 or ECDSA).
35+
* @returns The proof of possession.
36+
*/
37+
async createProofOfPossessionWithSigType(
38+
bech32Address: string,
39+
stakerBtcAddress: string,
40+
sigType: BTCSigType
41+
): Promise<ProofOfPossessionBTC> {
42+
const signedBabylonAddress = await this.btcProvider.signMessage(
43+
vendor.SigningStep.PROOF_OF_POSSESSION,
44+
bech32Address,
45+
sigType === BTCSigType.BIP322 ? 'bip322-simple' : 'ecdsa'
46+
);
47+
48+
let btcSig: Uint8Array;
49+
if (sigType === BTCSigType.BIP322) {
50+
const bip322Sig = BIP322Sig.fromPartial({
51+
address: stakerBtcAddress,
52+
sig: Buffer.from(signedBabylonAddress, 'base64'),
53+
});
54+
// Encode the BIP322 protobuf message to a Uint8Array
55+
btcSig = BIP322Sig.encode(bip322Sig).finish();
56+
} else {
57+
// Encode the ECDSA signature to a Uint8Array
58+
btcSig = Buffer.from(signedBabylonAddress, 'base64');
59+
}
60+
61+
return {
62+
btcSigType: sigType,
63+
btcSig,
64+
};
65+
}
66+
67+
/**
68+
* Creates a proof of possession for the staker based on ECDSA signature.
69+
* @param bech32Address - The staker's bech32 address on the babylon network.
70+
* @param stakerBtcAddress
71+
* @returns The proof of possession.
72+
*/
73+
async createProofOfPossession(bech32Address: string, stakerBtcAddress: string): Promise<ProofOfPossessionBTC> {
74+
// force the ECDSA signature type
75+
return this.createProofOfPossessionWithSigType(bech32Address, stakerBtcAddress, BTCSigType.ECDSA);
76+
}
77+
}
78+
779
export const mockBabylonProvider: vendor.BabylonProvider = {
880
signTransaction(): Promise<Uint8Array> {
981
throw new Error('Function not implemented.');
@@ -31,10 +103,5 @@ export function createStakingManager(
31103
throw new Error('Unsupported network');
32104
}
33105
}
34-
return new vendor.BabylonBtcStakingManager(
35-
network,
36-
stakingParams ?? getStakingParams(network),
37-
btcProvider,
38-
babylonProvider
39-
);
106+
return new BitGoStakingManager(network, stakingParams ?? getStakingParams(network), btcProvider, babylonProvider);
40107
}

modules/utxo-staking/test/unit/babylon/transactions.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,16 @@ function parseScripts(scripts: unknown) {
178178
return Object.fromEntries(Object.entries(scripts).map(([key, value]) => [key, parseScript(key, value)]));
179179
}
180180

181-
async function assertEqualsFixture(fixtureName: string, value: unknown): Promise<void> {
182-
value = normalize(value);
183-
assert.deepStrictEqual(await getFixture(fixtureName, value), value);
181+
type EqualsAssertion = typeof assert.deepStrictEqual;
182+
183+
async function assertEqualsFixture(
184+
fixtureName: string,
185+
value: unknown,
186+
n = normalize,
187+
eq: EqualsAssertion = assert.deepStrictEqual
188+
): Promise<void> {
189+
value = n(value);
190+
eq(await getFixture(fixtureName, value), value);
184191
}
185192

186193
async function assertScriptsEqualFixture(
@@ -379,7 +386,20 @@ function describeWithKeys(
379386
utxo,
380387
feeRateSatB,
381388
800_000
382-
)
389+
),
390+
normalize,
391+
(a, b) => {
392+
// The vendor library serializes the signature as BIP322, while
393+
// our implementation serializes it as ECDSA.
394+
// Strip the pop field from the MsgCreateBTCDelegation.
395+
function stripPop(v: unknown) {
396+
const vAny = v as any;
397+
delete vAny['unsignedDelegationMsg']['value']['pop'];
398+
}
399+
stripPop(a);
400+
stripPop(b);
401+
assert.deepStrictEqual(a, b);
402+
}
383403
);
384404
}
385405
});

0 commit comments

Comments
 (0)