Skip to content

Commit 071f7e4

Browse files
feat(utxo-staking): add utils for creating unsigned delegation messages
Added utilities to support unsigned delegation message creation including stakingParams functions, conversion between parameter formats, and helpers to create descriptor builders from common parameter sources. Issue: BTC-1826
1 parent 7fb9d68 commit 071f7e4

File tree

8 files changed

+235
-101
lines changed

8 files changed

+235
-101
lines changed

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

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { toWrappedPsbt } from '@bitgo/utxo-core/descriptor';
99

1010
import { BabylonDescriptorBuilder } from './descriptor';
1111

12+
export const mockBabylonProvider: vendor.BabylonProvider = {
13+
signTransaction(): Promise<Uint8Array> {
14+
throw new Error('Function not implemented.');
15+
},
16+
};
17+
1218
export type ValueWithTypeUrl<T> = { typeUrl: string; value: T };
1319

1420
export function getSignedPsbt(
@@ -76,6 +82,11 @@ export function getBtcProviderForECKey(
7682
};
7783
}
7884

85+
type Result = {
86+
unsignedDelegationMsg: ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>;
87+
stakingTx: bitcoinjslib.Transaction;
88+
};
89+
7990
/*
8091
* This is mostly lifted from
8192
* https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/staking/manager.ts#L100-L172
@@ -90,12 +101,9 @@ export async function createUnsignedPreStakeRegistrationBabylonTransaction(
90101
stakingInput: vendor.StakingInputs,
91102
babylonBtcTipHeight: number,
92103
inputUTXOs: vendor.UTXO[],
93-
feeRate: number,
104+
feeRateSatB: number,
94105
babylonAddress: string
95-
): Promise<{
96-
unsignedDelegationMsg: ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>;
97-
stakingTx: bitcoinjslib.Transaction;
98-
}> {
106+
): Promise<Result> {
99107
if (babylonBtcTipHeight === 0) {
100108
throw new Error('Babylon BTC tip height cannot be 0');
101109
}
@@ -118,7 +126,7 @@ export async function createUnsignedPreStakeRegistrationBabylonTransaction(
118126
);
119127

120128
// Create unsigned staking transaction
121-
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate);
129+
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRateSatB);
122130

123131
// Create delegation message without including inclusion proof
124132
const msg = await manager.createBtcDelegationMsg(
@@ -134,3 +142,28 @@ export async function createUnsignedPreStakeRegistrationBabylonTransaction(
134142
stakingTx: transaction,
135143
};
136144
}
145+
146+
export async function createUnsignedPreStakeRegistrationBabylonTransactionWithBtcProvider(
147+
btcProvider: vendor.BtcProvider,
148+
stakingParams: vendor.VersionedStakingParams,
149+
network: bitcoinjslib.Network,
150+
stakerBtcInfo: vendor.StakerInfo,
151+
stakingInput: vendor.StakingInputs,
152+
babylonBtcTipHeight: number,
153+
inputUTXOs: vendor.UTXO[],
154+
feeRateSatB: number,
155+
babylonAddress: string
156+
): Promise<Result> {
157+
const manager = new vendor.BabylonBtcStakingManager(network, [stakingParams], btcProvider, mockBabylonProvider);
158+
return await createUnsignedPreStakeRegistrationBabylonTransaction(
159+
manager,
160+
[stakingParams],
161+
network,
162+
stakerBtcInfo,
163+
stakingInput,
164+
babylonBtcTipHeight,
165+
inputUTXOs,
166+
feeRateSatB,
167+
babylonAddress
168+
);
169+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { Descriptor, ast } from '@bitgo/wasm-miniscript';
7+
import { StakingParams } from '@bitgo/babylonlabs-io-btc-staking-ts';
78

89
export function getUnspendableKey(): string {
910
// https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/constants/internalPubkey.ts
@@ -37,6 +38,37 @@ export class BabylonDescriptorBuilder {
3738
public unbondingTimeLock: number
3839
) {}
3940

41+
static fromParams(
42+
params: {
43+
stakerKey: Buffer;
44+
finalityProviderKeys: Buffer[];
45+
} & StakingParams
46+
): BabylonDescriptorBuilder {
47+
/*
48+
49+
const stakerKey = getECKey('staker');
50+
const covenantThreshold = stakingParams.covenantQuorum;
51+
const stakingTimelock = stakingParams.minStakingTimeBlocks;
52+
const unbondingTimelock = stakingParams.unbondingTime;
53+
const vendorBuilder = new vendor.StakingScriptData(
54+
getXOnlyPubkey(stakerKey),
55+
finalityProviderKeys.map(getXOnlyPubkey),
56+
covenantKeys.map(getXOnlyPubkey),
57+
covenantThreshold,
58+
stakingTimelock,
59+
unbondingTimelock
60+
);
61+
*/
62+
return new BabylonDescriptorBuilder(
63+
params.stakerKey,
64+
params.finalityProviderKeys,
65+
params.covenantNoCoordPks.map((k) => Buffer.from(k, 'hex')),
66+
params.covenantQuorum,
67+
params.minStakingTimeBlocks,
68+
params.unbondingTime
69+
);
70+
}
71+
4072
getTimelockMiniscript(): ast.MiniscriptNode {
4173
return { and_v: [pk(this.stakerKey), { older: this.stakingTimeLock }] };
4274
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export * from './delegationMessage';
22
export * from './descriptor';
3-
export * from './testnet';
3+
export * from './stakingParams';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ECPairInterface } from '@bitgo/utxo-lib';
2+
3+
export function getXOnlyPubkey(key: ECPairInterface): Buffer {
4+
return key.publicKey.subarray(1);
5+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { BIP32Interface, ECPairInterface } from '@bitgo/utxo-lib';
2+
import { StakerInfo, StakingInputs, VersionedStakingParams } from '@bitgo/babylonlabs-io-btc-staking-ts';
3+
4+
import { BabylonDescriptorBuilder } from './descriptor';
5+
6+
export type JsonParams = {
7+
version: number;
8+
covenant_pks: string[];
9+
covenant_quorum: number;
10+
min_staking_value_sat: number;
11+
max_staking_value_sat: number;
12+
min_staking_time_blocks: number;
13+
max_staking_time_blocks: number;
14+
slashing_pk_script: string;
15+
min_slashing_tx_fee_sat: number;
16+
slashing_rate: string;
17+
unbonding_time_blocks: number;
18+
unbonding_fee_sat: number;
19+
min_commission_rate: string;
20+
delegation_creation_base_gas_fee: number;
21+
allow_list_expiration_height: number;
22+
btc_activation_height: number;
23+
};
24+
25+
// Source: https://github.com/babylonlabs-io/babylon/blob/v1.99.0-snapshot.250211/app/upgrades/v1/testnet/btcstaking_params.go#L149-L159
26+
export const testnetStakingParams: JsonParams = {
27+
version: 5 /* it's the sixth element in the array */,
28+
covenant_pks: [
29+
'fa9d882d45f4060bdb8042183828cd87544f1ea997380e586cab77d5fd698737',
30+
'0aee0509b16db71c999238a4827db945526859b13c95487ab46725357c9a9f25',
31+
'17921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4',
32+
'113c3a32a9d320b72190a04a020a0db3976ef36972673258e9a38a364f3dc3b0',
33+
'79a71ffd71c503ef2e2f91bccfc8fcda7946f4653cef0d9f3dde20795ef3b9f0',
34+
'3bb93dfc8b61887d771f3630e9a63e97cbafcfcc78556a474df83a31a0ef899c',
35+
'd21faf78c6751a0d38e6bd8028b907ff07e9a869a43fc837d6b3f8dff6119a36',
36+
'40afaf47c4ffa56de86410d8e47baa2bb6f04b604f4ea24323737ddc3fe092df',
37+
'f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8e',
38+
],
39+
covenant_quorum: 6,
40+
min_staking_value_sat: 50000,
41+
max_staking_value_sat: 35000000000,
42+
min_staking_time_blocks: 10000,
43+
max_staking_time_blocks: 64000,
44+
slashing_pk_script: 'ABRb4SYk0IorQkCV18ByIcM0UNFL8Q==',
45+
min_slashing_tx_fee_sat: 5000,
46+
slashing_rate: '0.05',
47+
unbonding_time_blocks: 1008,
48+
unbonding_fee_sat: 2000,
49+
min_commission_rate: '0.03',
50+
delegation_creation_base_gas_fee: 1095000,
51+
allow_list_expiration_height: 26124,
52+
btc_activation_height: 227174,
53+
};
54+
55+
// https://github.com/babylonlabs-io/babylon/blob/v1.99.0-snapshot.250211/app/upgrades/v1/mainnet/btcstaking_params.go#L4-L28
56+
export const mainnetStakingParams: JsonParams = {
57+
version: 0,
58+
covenant_pks: [
59+
'43311589af63c2adda04fcd7792c038a05c12a4fe40351b3eb1612ff6b2e5a0e',
60+
'd415b187c6e7ce9da46ac888d20df20737d6f16a41639e68ea055311e1535dd9',
61+
'd27cd27dbff481bc6fc4aa39dd19405eb6010237784ecba13bab130a4a62df5d',
62+
'a3e107fee8879f5cf901161dbf4ff61c252ba5fec6f6407fe81b9453d244c02c',
63+
'c45753e856ad0abb06f68947604f11476c157d13b7efd54499eaa0f6918cf716',
64+
],
65+
covenant_quorum: 3,
66+
min_staking_value_sat: 10000,
67+
max_staking_value_sat: 10000000000,
68+
min_staking_time_blocks: 10,
69+
max_staking_time_blocks: 65535,
70+
slashing_pk_script: 'dqkUAQEBAQEBAQEBAQEBAQEBAQEBAQGIrA==',
71+
min_slashing_tx_fee_sat: 1000,
72+
slashing_rate: '0.100000000000000000',
73+
unbonding_time_blocks: 101,
74+
unbonding_fee_sat: 1000,
75+
min_commission_rate: '0.03',
76+
delegation_creation_base_gas_fee: 1000,
77+
allow_list_expiration_height: 0,
78+
btc_activation_height: 100,
79+
};
80+
81+
export function toVersionedParams(p: JsonParams): VersionedStakingParams {
82+
return {
83+
version: p.version,
84+
btcActivationHeight: p.btc_activation_height,
85+
covenantNoCoordPks: p.covenant_pks,
86+
covenantQuorum: p.covenant_quorum,
87+
unbondingTime: p.unbonding_time_blocks,
88+
unbondingFeeSat: p.unbonding_fee_sat,
89+
maxStakingAmountSat: p.max_staking_value_sat,
90+
minStakingAmountSat: p.min_staking_value_sat,
91+
maxStakingTimeBlocks: p.max_staking_time_blocks,
92+
minStakingTimeBlocks: p.min_staking_time_blocks,
93+
slashing: {
94+
slashingPkScriptHex: Buffer.from(p.slashing_pk_script, 'base64').toString('hex'),
95+
slashingRate: parseFloat(p.slashing_rate),
96+
minSlashingTxFeeSat: p.min_slashing_tx_fee_sat,
97+
},
98+
};
99+
}
100+
101+
// Source: https://btcstaking.testnet.babylonlabs.io/ "Babylon Foundation 0"
102+
export const testnetFinalityProvider0 = Buffer.from(
103+
'd23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76',
104+
'hex'
105+
);
106+
107+
export function getDescriptorBuilderForParams(
108+
userKey: BIP32Interface | ECPairInterface | Buffer,
109+
finalityProviderKeys: Buffer[],
110+
params: Pick<
111+
VersionedStakingParams,
112+
'covenantNoCoordPks' | 'covenantQuorum' | 'minStakingTimeBlocks' | 'unbondingTime'
113+
>
114+
): BabylonDescriptorBuilder {
115+
if (!Buffer.isBuffer(userKey)) {
116+
userKey = userKey.publicKey;
117+
}
118+
return new BabylonDescriptorBuilder(
119+
userKey,
120+
finalityProviderKeys,
121+
params.covenantNoCoordPks.map((pk) => Buffer.from(pk, 'hex')),
122+
params.covenantQuorum,
123+
params.minStakingTimeBlocks,
124+
params.unbondingTime
125+
);
126+
}
127+
128+
export function getDescriptorProviderForStakingParams(
129+
stakerBtcInfo: StakerInfo,
130+
stakingInput: StakingInputs,
131+
stakingParams: VersionedStakingParams
132+
): BabylonDescriptorBuilder {
133+
const userKey = Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, 'hex');
134+
const finalityProviderKey = Buffer.from(stakingInput.finalityProviderPkNoCoordHex, 'hex');
135+
return getDescriptorBuilderForParams(userKey, [finalityProviderKey], stakingParams);
136+
}
137+
138+
export function getTestnetDescriptorBuilder(
139+
userKey: BIP32Interface | ECPairInterface | Buffer,
140+
{
141+
finalityProviderKeys = [testnetFinalityProvider0],
142+
}: {
143+
finalityProviderKeys?: Buffer[];
144+
} = {}
145+
): BabylonDescriptorBuilder {
146+
return getDescriptorBuilderForParams(userKey, finalityProviderKeys, toVersionedParams(testnetStakingParams));
147+
}

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

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

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

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import { getFixture, toPlainObject } from '@bitgo/utxo-core/testutil';
1010

1111
import {
1212
BabylonDescriptorBuilder,
13-
finalityBabylonProvider0,
13+
testnetFinalityProvider0,
1414
getSignedPsbt,
1515
testnetStakingParams,
16+
toVersionedParams,
1617
} from '../../../src/babylon';
1718
import { normalize } from '../fixtures.utils';
1819

@@ -21,26 +22,6 @@ import { getBitGoUtxoStakingMsgCreateBtcDelegation, getVendorMsgCreateBtcDelegat
2122

2223
bitcoinjslib.initEccLib(utxolib.ecc);
2324

24-
function getTestnetStakingParams(): vendor.VersionedStakingParams {
25-
return {
26-
version: testnetStakingParams.version,
27-
btcActivationHeight: testnetStakingParams.btc_activation_height,
28-
covenantNoCoordPks: testnetStakingParams.covenant_pks,
29-
covenantQuorum: testnetStakingParams.covenant_quorum,
30-
unbondingTime: testnetStakingParams.unbonding_time_blocks,
31-
unbondingFeeSat: testnetStakingParams.unbonding_fee_sat,
32-
maxStakingAmountSat: testnetStakingParams.max_staking_value_sat,
33-
minStakingAmountSat: testnetStakingParams.min_staking_value_sat,
34-
maxStakingTimeBlocks: testnetStakingParams.max_staking_time_blocks,
35-
minStakingTimeBlocks: testnetStakingParams.min_staking_time_blocks,
36-
slashing: {
37-
slashingPkScriptHex: Buffer.from(testnetStakingParams.slashing_pk_script, 'base64').toString('hex'),
38-
slashingRate: parseFloat(testnetStakingParams.slashing_rate),
39-
minSlashingTxFeeSat: testnetStakingParams.min_slashing_tx_fee_sat,
40-
},
41-
};
42-
}
43-
4425
type WithFee<T> = T & { fee: number };
4526
type TransactionWithFee = WithFee<{ transaction: bitcoinjslib.Transaction }>;
4627
type PsbtWithFee = WithFee<{ psbt: bitcoinjslib.Psbt }>;
@@ -111,7 +92,7 @@ function getStakingTransactionTreeVendor(
11192

11293
function getTestnetStakingParamsWithCovenant(covenantKeys: ECPairInterface[]): vendor.VersionedStakingParams {
11394
return {
114-
...getTestnetStakingParams(),
95+
...toVersionedParams(testnetStakingParams),
11596
covenantNoCoordPks: covenantKeys.map((pk) => getXOnlyPubkey(pk).toString('hex')),
11697
};
11798
}
@@ -379,5 +360,9 @@ function describeWithMockKeys(tag: string, finalityProviderKeys: ECPairInterface
379360
});
380361
}
381362

382-
describeWithKeysFromStakingParams('testnet', [fromXOnlyPublicKey(finalityBabylonProvider0)], getTestnetStakingParams());
363+
describeWithKeysFromStakingParams(
364+
'testnet',
365+
[fromXOnlyPublicKey(testnetFinalityProvider0)],
366+
toVersionedParams(testnetStakingParams)
367+
);
383368
describeWithMockKeys('testnetMock', getECKeys('finalityProvider', 1), getECKeys('covenant', 9));

0 commit comments

Comments
 (0)