Skip to content

Commit 2ba0145

Browse files
Merge pull request #5822 from BitGo/BTC-1933.createDelegationMessageFromPsbt
feat(utxo-staking): implement Babylon staking utilities
2 parents e82a3e3 + 689aded commit 2ba0145

File tree

8 files changed

+163
-104
lines changed

8 files changed

+163
-104
lines changed

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

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
1+
/**
2+
* https://github.com/babylonlabs-io/babylon/blob/v1.99.0-snapshot.250211/x/btcstaking/types/validate_parsed_message.go
3+
*/
14
import assert from 'assert';
25

36
import * as vendor from '@bitgo/babylonlabs-io-btc-staking-ts';
47
import * as babylonProtobuf from '@babylonlabs-io/babylon-proto-ts';
58
import * as bitcoinjslib from 'bitcoinjs-lib';
69
import * as utxolib from '@bitgo/utxo-lib';
710
import { Descriptor } from '@bitgo/wasm-miniscript';
11+
import { toXOnlyPublicKey } from '@bitgo/utxo-core';
812
import { toWrappedPsbt } from '@bitgo/utxo-core/descriptor';
913

1014
import { BabylonDescriptorBuilder } from './descriptor';
1115
import { createStakingManager } from './stakingManager';
1216
import { getStakingParams } from './stakingParams';
17+
import { BabylonNetworkLike, toBitcoinJsNetwork } from './network';
1318

1419
export type ValueWithTypeUrl<T> = { typeUrl: string; value: T };
1520

@@ -83,60 +88,106 @@ type Result = {
8388
stakingTx: bitcoinjslib.Transaction;
8489
};
8590

86-
/*
87-
* This is mostly lifted from
88-
* https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/staking/manager.ts#L100-L172
89-
*
90-
* The difference is that here we are returning an _unsigned_ delegation message.
91+
/**
92+
* @param stakingKey - this is the single-sig key that is used for co-signing the staking output
93+
* @param changeAddress - this is unrelated to the staking key and is used for the change output
9194
*/
92-
export async function createUnsignedPreStakeRegistrationBabylonTransaction(
93-
manager: vendor.BabylonBtcStakingManager,
94-
stakingParams: vendor.VersionedStakingParams[],
95-
network: bitcoinjslib.Network,
95+
export function toStakerInfo(
96+
stakingKey: utxolib.ECPairInterface | Buffer | string,
97+
changeAddress: string
98+
): vendor.StakerInfo {
99+
if (typeof stakingKey === 'object' && 'publicKey' in stakingKey) {
100+
stakingKey = stakingKey.publicKey;
101+
}
102+
if (typeof stakingKey === 'string') {
103+
stakingKey = Buffer.from(stakingKey, 'hex');
104+
}
105+
return {
106+
publicKeyNoCoordHex: toXOnlyPublicKey(stakingKey).toString('hex'),
107+
address: changeAddress,
108+
};
109+
}
110+
111+
export function createStaking(
112+
network: BabylonNetworkLike,
113+
blockHeight: number,
96114
stakerBtcInfo: vendor.StakerInfo,
97115
stakingInput: vendor.StakingInputs,
98-
babylonBtcTipHeight: number,
99-
inputUTXOs: vendor.UTXO[],
100-
feeRateSatB: number,
101-
babylonAddress: string
102-
): Promise<Result> {
103-
if (babylonBtcTipHeight === 0) {
116+
versionedParams: vendor.VersionedStakingParams[] = getStakingParams(network)
117+
): vendor.Staking {
118+
if (blockHeight === 0) {
104119
throw new Error('Babylon BTC tip height cannot be 0');
105120
}
106-
if (inputUTXOs.length === 0) {
107-
throw new Error('No input UTXOs provided');
108-
}
109-
if (!vendor.isValidBabylonAddress(babylonAddress)) {
110-
throw new Error('Invalid Babylon address');
111-
}
112121

113122
// Get the Babylon params based on the BTC tip height from Babylon chain
114-
const params = vendor.getBabylonParamByBtcHeight(babylonBtcTipHeight, stakingParams);
123+
const params = vendor.getBabylonParamByBtcHeight(blockHeight, versionedParams);
115124

116-
const staking = new vendor.Staking(
117-
network,
125+
return new vendor.Staking(
126+
toBitcoinJsNetwork(network),
118127
stakerBtcInfo,
119128
params,
120129
stakingInput.finalityProviderPkNoCoordHex,
121130
stakingInput.stakingTimelock
122131
);
132+
}
123133

124-
// Create unsigned staking transaction
125-
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRateSatB);
134+
type TransactionLike =
135+
| bitcoinjslib.Psbt
136+
| bitcoinjslib.Transaction
137+
| utxolib.Transaction
138+
| utxolib.bitgo.UtxoTransaction<bigint | number>
139+
| utxolib.Psbt
140+
| utxolib.bitgo.UtxoPsbt;
126141

142+
function toStakingTransactionFromPsbt(
143+
psbt: bitcoinjslib.Psbt | utxolib.Psbt | utxolib.bitgo.UtxoPsbt
144+
): bitcoinjslib.Transaction {
145+
if (!(psbt instanceof utxolib.bitgo.UtxoPsbt)) {
146+
psbt = utxolib.bitgo.createPsbtFromBuffer(psbt.toBuffer(), utxolib.networks.bitcoin);
147+
}
148+
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
149+
// only utxolib.bitgo.UtxoPsbt has the getUnsignedTx method
150+
return bitcoinjslib.Transaction.fromHex(psbt.getUnsignedTx().toHex());
151+
}
152+
throw new Error('illegal state');
153+
}
154+
155+
export function toStakingTransaction(tx: TransactionLike): bitcoinjslib.Transaction {
156+
if (tx instanceof bitcoinjslib.Psbt || tx instanceof utxolib.Psbt) {
157+
return toStakingTransactionFromPsbt(tx);
158+
}
159+
return bitcoinjslib.Transaction.fromHex(tx.toHex());
160+
}
161+
162+
/*
163+
* This is mostly lifted from
164+
* https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/staking/manager.ts#L100-L172
165+
*
166+
* The difference is that here we are returning an _unsigned_ delegation message.
167+
*/
168+
export async function createDelegationMessageWithTransaction(
169+
manager: vendor.BabylonBtcStakingManager,
170+
staking: vendor.Staking,
171+
stakingAmountSat: number,
172+
transaction: TransactionLike,
173+
babylonAddress: string
174+
): Promise<ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>> {
175+
if (!vendor.isValidBabylonAddress(babylonAddress)) {
176+
throw new Error('Invalid Babylon address');
177+
}
127178
// Create delegation message without including inclusion proof
128-
const msg = await manager.createBtcDelegationMsg(
179+
return manager.createBtcDelegationMsg(
129180
staking,
130-
stakingInput,
131-
transaction,
181+
{
182+
stakingTimelock: staking.stakingTimelock,
183+
finalityProviderPkNoCoordHex: staking.finalityProviderPkNoCoordHex,
184+
stakingAmountSat,
185+
},
186+
toStakingTransaction(transaction),
132187
babylonAddress,
133-
stakerBtcInfo,
134-
params
188+
staking.stakerInfo,
189+
staking.params
135190
);
136-
return {
137-
unsignedDelegationMsg: msg,
138-
stakingTx: transaction,
139-
};
140191
}
141192

142193
export async function createUnsignedPreStakeRegistrationBabylonTransactionWithBtcProvider(
@@ -150,16 +201,19 @@ export async function createUnsignedPreStakeRegistrationBabylonTransactionWithBt
150201
babylonAddress: string,
151202
stakingParams: vendor.VersionedStakingParams[] = getStakingParams(network)
152203
): Promise<Result> {
204+
if (inputUTXOs.length === 0) {
205+
throw new Error('No input UTXOs provided');
206+
}
153207
const manager = createStakingManager(network, btcProvider, stakingParams);
154-
return await createUnsignedPreStakeRegistrationBabylonTransaction(
208+
const staking = createStaking(network, babylonBtcTipHeight, stakerBtcInfo, stakingInput, stakingParams);
209+
// Create unsigned staking transaction
210+
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRateSatB);
211+
const unsignedDelegationMsg = await createDelegationMessageWithTransaction(
155212
manager,
156-
stakingParams,
157-
network,
158-
stakerBtcInfo,
159-
stakingInput,
160-
babylonBtcTipHeight,
161-
inputUTXOs,
162-
feeRateSatB,
213+
staking,
214+
stakingInput.stakingAmountSat,
215+
transaction,
163216
babylonAddress
164217
);
218+
return { unsignedDelegationMsg, stakingTx: transaction };
165219
}

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

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* https://github.com/babylonlabs-io/babylon/tree/main/docs
33
* https://github.com/babylonlabs-io/babylon/blob/main/docs/staking-script.md
4+
* https://github.com/babylonlabs-io/babylon/blob/v1.99.0-snapshot.250211/btcstaking/staking.go
45
*/
56

67
import { Descriptor, ast } from '@bitgo/wasm-miniscript';
@@ -44,21 +45,6 @@ export class BabylonDescriptorBuilder {
4445
finalityProviderKeys: Buffer[];
4546
} & StakingParams
4647
): 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-
*/
6248
return new BabylonDescriptorBuilder(
6349
params.stakerKey,
6450
params.finalityProviderKeys,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { initBTCCurve } from '@bitgo/babylonlabs-io-btc-staking-ts';
2+
import * as bitcoinjslib from 'bitcoinjs-lib';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
5+
initBTCCurve();
6+
bitcoinjslib.initEccLib(utxolib.ecc);
7+
18
export * from './delegationMessage';
29
export * from './descriptor';
310
export * from './stakingParams';

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

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as bitcoinjslib from 'bitcoinjs-lib';
2+
import * as utxolib from '@bitgo/utxo-lib';
3+
4+
export type BabylonNetwork = 'mainnet' | 'testnet';
5+
6+
export type BabylonNetworkLike = bitcoinjslib.Network | utxolib.Network | BabylonNetwork;
7+
8+
export function toBabylonNetwork(n: BabylonNetworkLike): BabylonNetwork {
9+
switch (n) {
10+
case bitcoinjslib.networks.bitcoin:
11+
case utxolib.networks.bitcoin:
12+
return 'mainnet';
13+
case bitcoinjslib.networks.testnet:
14+
case utxolib.networks.testnet:
15+
case utxolib.networks.bitcoinPublicSignet:
16+
return 'testnet';
17+
case 'mainnet':
18+
case 'testnet':
19+
return n;
20+
default:
21+
throw new Error('Unsupported network');
22+
}
23+
}
24+
25+
export function toBitcoinJsNetwork(n: BabylonNetworkLike): bitcoinjslib.Network {
26+
switch (toBabylonNetwork(n)) {
27+
case 'mainnet':
28+
return bitcoinjslib.networks.bitcoin;
29+
case 'testnet':
30+
return bitcoinjslib.networks.testnet;
31+
default:
32+
throw new Error('Unsupported network');
33+
}
34+
}

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

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ import * as t from 'io-ts';
22
import * as tt from 'io-ts-types';
33
import { isLeft } from 'fp-ts/Either';
44
import { PathReporter } from 'io-ts/lib/PathReporter';
5-
import * as bitcoinjslib from 'bitcoinjs-lib';
65
import * as utxolib from '@bitgo/utxo-lib';
76
import {
87
getBabylonParamByVersion,
98
StakerInfo,
109
StakingInputs,
10+
StakingParams,
1111
VersionedStakingParams,
1212
} from '@bitgo/babylonlabs-io-btc-staking-ts';
13+
export { getBabylonParamByVersion, getBabylonParamByBtcHeight } from '@bitgo/babylonlabs-io-btc-staking-ts';
1314

1415
import { BabylonDescriptorBuilder } from './descriptor';
1516
import jsonMainnetParams from './params.mainnet.json';
1617
import jsonTestnetParams from './params.testnet.json';
18+
import { BabylonNetworkLike, toBabylonNetwork } from './network';
1719

1820
const BabylonParamsJSON = t.type({
1921
covenant_pks: t.array(t.string),
@@ -68,27 +70,6 @@ function toVersionedParamsFromJson(jsonParams: unknown[]): VersionedStakingParam
6870
);
6971
}
7072

71-
type BabylonNetwork = 'mainnet' | 'testnet';
72-
73-
type BabylonNetworkLike = bitcoinjslib.Network | utxolib.Network | BabylonNetwork;
74-
75-
function toBabylonNetwork(n: BabylonNetworkLike): BabylonNetwork {
76-
switch (n) {
77-
case bitcoinjslib.networks.bitcoin:
78-
case utxolib.networks.bitcoin:
79-
return 'mainnet';
80-
case bitcoinjslib.networks.testnet:
81-
case utxolib.networks.testnet:
82-
case utxolib.networks.bitcoinPublicSignet:
83-
return 'testnet';
84-
case 'mainnet':
85-
case 'testnet':
86-
return n;
87-
default:
88-
throw new Error('Unsupported network');
89-
}
90-
}
91-
9273
export const mainnetStakingParams: readonly VersionedStakingParams[] = Object.freeze(
9374
toVersionedParamsFromJson(jsonMainnetParams)
9475
);
@@ -114,13 +95,16 @@ export const testnetFinalityProvider0 = Buffer.from(
11495
'hex'
11596
);
11697

98+
type DescriptorStakingParams = Pick<
99+
StakingParams,
100+
'covenantNoCoordPks' | 'covenantQuorum' | 'minStakingTimeBlocks' | 'unbondingTime'
101+
>;
102+
117103
export function getDescriptorBuilderForParams(
118104
userKey: utxolib.BIP32Interface | utxolib.ECPairInterface | Buffer,
119105
finalityProviderKeys: Buffer[],
120-
params: Pick<
121-
VersionedStakingParams,
122-
'covenantNoCoordPks' | 'covenantQuorum' | 'minStakingTimeBlocks' | 'unbondingTime'
123-
>
106+
stakingTimelock: number,
107+
params: DescriptorStakingParams
124108
): BabylonDescriptorBuilder {
125109
if (!Buffer.isBuffer(userKey)) {
126110
userKey = userKey.publicKey;
@@ -130,32 +114,32 @@ export function getDescriptorBuilderForParams(
130114
finalityProviderKeys,
131115
params.covenantNoCoordPks.map((pk) => Buffer.from(pk, 'hex')),
132116
params.covenantQuorum,
133-
params.minStakingTimeBlocks,
117+
stakingTimelock,
134118
params.unbondingTime
135119
);
136120
}
137121

138122
export function getDescriptorProviderForStakingParams(
139-
stakerBtcInfo: StakerInfo,
123+
stakerBtcInfo: Pick<StakerInfo, 'publicKeyNoCoordHex'>,
140124
stakingInput: StakingInputs,
141-
stakingParams: VersionedStakingParams
125+
stakingParams: DescriptorStakingParams
142126
): BabylonDescriptorBuilder {
143127
const userKey = Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, 'hex');
144128
const finalityProviderKey = Buffer.from(stakingInput.finalityProviderPkNoCoordHex, 'hex');
145-
return getDescriptorBuilderForParams(userKey, [finalityProviderKey], stakingParams);
129+
return getDescriptorBuilderForParams(userKey, [finalityProviderKey], stakingInput.stakingTimelock, stakingParams);
146130
}
147131

148132
export function getTestnetDescriptorBuilder(
149133
userKey: utxolib.BIP32Interface | utxolib.ECPairInterface | Buffer,
150134
{
151135
finalityProviderKeys = [testnetFinalityProvider0],
136+
params = getBabylonParamByVersion(5, getStakingParams('testnet')),
137+
stakingTimelock = params.minStakingTimeBlocks,
152138
}: {
153139
finalityProviderKeys?: Buffer[];
140+
params?: StakingParams;
141+
stakingTimelock?: number;
154142
} = {}
155143
): BabylonDescriptorBuilder {
156-
return getDescriptorBuilderForParams(
157-
userKey,
158-
finalityProviderKeys,
159-
getBabylonParamByVersion(5, getStakingParams('testnet'))
160-
);
144+
return getDescriptorBuilderForParams(userKey, finalityProviderKeys, stakingTimelock, params);
161145
}

modules/utxo-staking/src/transaction.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as utxolib from '@bitgo/utxo-lib';
22
import { Dimensions } from '@bitgo/unspents';
3+
import * as bitcoinjslib from 'bitcoinjs-lib';
4+
5+
bitcoinjslib.initEccLib(utxolib.ecc);
36

47
/**
58
* Build a staking transaction for a wallet that assumes 2-of-3 multisig for the inputs

0 commit comments

Comments
 (0)