Skip to content

Commit a8f5223

Browse files
authored
Merge pull request #5719 from BitGo/BTC-1826.custom-create-unsigned-delegation-msg-func
feat(utxo-staking): add unsigned delegation message creation
2 parents d5a07fa + 071f7e4 commit a8f5223

File tree

14 files changed

+495
-234
lines changed

14 files changed

+495
-234
lines changed

modules/babylonlabs-io-btc-staking-ts/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { ObservableStaking, ObservableStakingScriptData } from './staking/observ
44
export * from './staking/transactions';
55
export * from './types';
66
export * from './utils/btc';
7+
export * from './utils/babylon';
78
export * from './utils/staking';
89
export * from './utils/utxo/findInputUTXO';
910
export * from './utils/utxo/getPsbtInputFields';

modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ export class BabylonBtcStakingManager {
661661
* @param inclusionProof - The inclusion proof of the staking transaction.
662662
* @returns The protobuf message.
663663
*/
664-
private async createBtcDelegationMsg(
664+
public async createBtcDelegationMsg(
665665
stakingInstance: Staking,
666666
stakingInput: StakingInputs,
667667
stakingTx: Transaction,

modules/utxo-staking/package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,12 @@
3939
]
4040
},
4141
"dependencies": {
42+
"@bitgo/babylonlabs-io-btc-staking-ts": "^0.4.0-bitgo.1",
43+
"@babylonlabs-io/babylon-proto-ts": "1.0.0",
4244
"@bitgo/unspents": "^0.47.20",
4345
"@bitgo/utxo-core": "^1.5.0",
4446
"@bitgo/utxo-lib": "^11.2.4",
45-
"@bitgo/wasm-miniscript": "2.0.0-beta.7"
46-
},
47-
"devDependencies": {
48-
"@bitgo/babylonlabs-io-btc-staking-ts": "^0.4.0-bitgo.1",
49-
"@babylonlabs-io/babylon-proto-ts": "1.0.0",
47+
"@bitgo/wasm-miniscript": "2.0.0-beta.7",
5048
"bitcoinjs-lib": "^6.1.7"
5149
}
5250
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import assert from 'assert';
2+
3+
import * as vendor from '@bitgo/babylonlabs-io-btc-staking-ts';
4+
import * as babylonProtobuf from '@babylonlabs-io/babylon-proto-ts';
5+
import * as bitcoinjslib from 'bitcoinjs-lib';
6+
import { ECPairInterface } from '@bitgo/utxo-lib';
7+
import { Descriptor } from '@bitgo/wasm-miniscript';
8+
import { toWrappedPsbt } from '@bitgo/utxo-core/descriptor';
9+
10+
import { BabylonDescriptorBuilder } from './descriptor';
11+
12+
export const mockBabylonProvider: vendor.BabylonProvider = {
13+
signTransaction(): Promise<Uint8Array> {
14+
throw new Error('Function not implemented.');
15+
},
16+
};
17+
18+
export type ValueWithTypeUrl<T> = { typeUrl: string; value: T };
19+
20+
export function getSignedPsbt(
21+
psbt: bitcoinjslib.Psbt,
22+
descriptor: Descriptor,
23+
signers: ECPairInterface[],
24+
{ finalize = false }
25+
): bitcoinjslib.Psbt {
26+
const wrappedPsbt = toWrappedPsbt(psbt.toBuffer());
27+
const signedInputs = psbt.data.inputs.flatMap((input, i) => {
28+
assert(input.witnessUtxo);
29+
if (Buffer.from(descriptor.scriptPubkey()).equals(input.witnessUtxo.script)) {
30+
wrappedPsbt.updateInputWithDescriptor(i, descriptor);
31+
const signResults = signers.map((signer) => {
32+
assert(signer.privateKey);
33+
return wrappedPsbt.signWithPrv(signer.privateKey);
34+
});
35+
return [[i, signResults]];
36+
}
37+
return [];
38+
});
39+
assert(signedInputs.length > 0);
40+
if (finalize) {
41+
wrappedPsbt.finalize();
42+
}
43+
return bitcoinjslib.Psbt.fromBuffer(Buffer.from(wrappedPsbt.serialize()));
44+
}
45+
46+
export function getBtcProviderForECKey(
47+
descriptorBuilder: BabylonDescriptorBuilder,
48+
stakerKey: ECPairInterface
49+
): vendor.BtcProvider {
50+
function signWithDescriptor(
51+
psbt: bitcoinjslib.Psbt,
52+
descriptor: Descriptor,
53+
key: ECPairInterface
54+
): bitcoinjslib.Psbt {
55+
psbt = getSignedPsbt(psbt, descriptor, [key], { finalize: false });
56+
// BUG: we need to blindly finalize here even though we have not fully signed
57+
psbt.finalizeAllInputs();
58+
return psbt;
59+
}
60+
61+
return {
62+
async signMessage(signingStep: vendor.SigningStep, message: string, type: 'ecdsa'): Promise<string> {
63+
assert(type === 'ecdsa');
64+
switch (signingStep) {
65+
case 'proof-of-possession':
66+
return stakerKey.sign(Buffer.from(message, 'hex')).toString('hex');
67+
default:
68+
throw new Error(`unexpected signing step: ${signingStep}`);
69+
}
70+
},
71+
async signPsbt(signingStep: vendor.SigningStep, psbtHex: string): Promise<string> {
72+
const psbt = bitcoinjslib.Psbt.fromHex(psbtHex);
73+
switch (signingStep) {
74+
case 'staking-slashing':
75+
return signWithDescriptor(psbt, descriptorBuilder.getStakingDescriptor(), stakerKey).toHex();
76+
case 'unbonding-slashing':
77+
return signWithDescriptor(psbt, descriptorBuilder.getUnbondingDescriptor(), stakerKey).toHex();
78+
default:
79+
throw new Error(`unexpected signing step: ${signingStep}`);
80+
}
81+
},
82+
};
83+
}
84+
85+
type Result = {
86+
unsignedDelegationMsg: ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>;
87+
stakingTx: bitcoinjslib.Transaction;
88+
};
89+
90+
/*
91+
* This is mostly lifted from
92+
* https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/staking/manager.ts#L100-L172
93+
*
94+
* The difference is that here we are returning an _unsigned_ delegation message.
95+
*/
96+
export async function createUnsignedPreStakeRegistrationBabylonTransaction(
97+
manager: vendor.BabylonBtcStakingManager,
98+
stakingParams: vendor.VersionedStakingParams[],
99+
network: bitcoinjslib.Network,
100+
stakerBtcInfo: vendor.StakerInfo,
101+
stakingInput: vendor.StakingInputs,
102+
babylonBtcTipHeight: number,
103+
inputUTXOs: vendor.UTXO[],
104+
feeRateSatB: number,
105+
babylonAddress: string
106+
): Promise<Result> {
107+
if (babylonBtcTipHeight === 0) {
108+
throw new Error('Babylon BTC tip height cannot be 0');
109+
}
110+
if (inputUTXOs.length === 0) {
111+
throw new Error('No input UTXOs provided');
112+
}
113+
if (!vendor.isValidBabylonAddress(babylonAddress)) {
114+
throw new Error('Invalid Babylon address');
115+
}
116+
117+
// Get the Babylon params based on the BTC tip height from Babylon chain
118+
const params = vendor.getBabylonParamByBtcHeight(babylonBtcTipHeight, stakingParams);
119+
120+
const staking = new vendor.Staking(
121+
network,
122+
stakerBtcInfo,
123+
params,
124+
stakingInput.finalityProviderPkNoCoordHex,
125+
stakingInput.stakingTimelock
126+
);
127+
128+
// Create unsigned staking transaction
129+
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRateSatB);
130+
131+
// Create delegation message without including inclusion proof
132+
const msg = await manager.createBtcDelegationMsg(
133+
staking,
134+
stakingInput,
135+
transaction,
136+
babylonAddress,
137+
stakerBtcInfo,
138+
params
139+
);
140+
return {
141+
unsignedDelegationMsg: msg,
142+
stakingTx: transaction,
143+
};
144+
}
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export * from './delegationMessage';
12
export * from './descriptor';
2-
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+
}

0 commit comments

Comments
 (0)