Skip to content

Commit c8f312b

Browse files
OttoAllmendingerllm-git
andcommitted
feat(babylonlabs-io-btc-staking-ts): backport changes from v1.0.3
Add BIP-322 signature support for Taproot and Native SegWit addresses in the proof of possession generation. Previously only ECDSA signatures were supported for legacy addresses. Improves API documentation to better explain function parameters and required BTC fee rates. Fixes zero equality checks to handle negative values correctly. BTC-2032 Co-authored-by: llm-git <[email protected]>
1 parent 22b4e31 commit c8f312b

File tree

5 files changed

+142
-44
lines changed

5 files changed

+142
-44
lines changed

modules/babylonlabs-io-btc-staking-ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bitgo/babylonlabs-io-btc-staking-ts",
3-
"version": "0.5.0",
3+
"version": "1.0.0",
44
"description": "Library exposing methods for the creation and consumption of Bitcoin transactions pertaining to Babylon's Bitcoin Staking protocol.",
55
"module": "dist/index.js",
66
"main": "dist/index.cjs",

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

Lines changed: 74 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,53 @@ import { networks, Psbt, Transaction } from "bitcoinjs-lib";
22
import { StakingParams, VersionedStakingParams } from "../types/params";
33
import { TransactionResult, UTXO } from "../types";
44
import { StakerInfo, Staking } from ".";
5-
import { fromBech32 } from "@cosmjs/encoding";
65
import {
76
btccheckpoint,
87
btcstaking,
98
btcstakingtx,
109
} from "@babylonlabs-io/babylon-proto-ts";
1110
import {
11+
BIP322Sig,
1212
BTCSigType,
1313
ProofOfPossessionBTC,
1414
} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop";
1515
import { BABYLON_REGISTRY_TYPE_URLS } from "../constants/registry";
1616
import { createCovenantWitness } from "./transactions";
1717
import { getBabylonParamByBtcHeight, getBabylonParamByVersion } from "../utils/staking/param";
18-
import { reverseBuffer, uint8ArrayToHex } from "../utils";
18+
import { reverseBuffer } from "../utils";
1919
import { deriveStakingOutputInfo } from "../utils/staking";
2020
import { findMatchingTxOutputIndex } from "../utils/staking";
2121
import { isValidBabylonAddress } from "../utils/babylon";
2222
import { StakingError } from "../error";
2323
import { StakingErrorCode } from "../error";
24+
import { isNativeSegwit, isTaproot } from "../utils/btc";
2425

2526
export interface BtcProvider {
2627
// Sign a PSBT
28+
// Expecting the PSBT to be encoded in hex format.
2729
signPsbt(signingStep: SigningStep, psbtHex: string): Promise<string>;
28-
// Sign a message using the ECDSA type
29-
// This is optional and only required if you would like to use the
30-
// `createProofOfPossession` function
31-
signMessage?: (signingStep: SigningStep, message: string, type: "ecdsa") => Promise<string>;
30+
31+
// Signs a message using either ECDSA or BIP-322, depending on the address type.
32+
// - Taproot and Native Segwit addresses will use BIP-322.
33+
// - Legacy addresses will use ECDSA.
34+
// Expecting the message to be encoded in base64 format.
35+
signMessage: (
36+
signingStep: SigningStep, message: string, type: "ecdsa" | "bip322-simple"
37+
) => Promise<string>;
3238
}
3339

3440
export interface BabylonProvider {
41+
/**
42+
* Signs a Babylon chain transaction using the provided signing step.
43+
* This is primarily used for signing MsgCreateBTCDelegation transactions
44+
* which register the BTC delegation on the Babylon Genesis chain.
45+
*
46+
* @param {SigningStep} signingStep - The current signing step context
47+
* @param {object} msg - The Cosmos SDK transaction message to sign
48+
* @param {string} msg.typeUrl - The Protobuf type URL identifying the message type
49+
* @param {T} msg.value - The transaction message data matching the typeUrl
50+
* @returns {Promise<Uint8Array>} The signed transaction bytes
51+
*/
3552
signTransaction: <T extends object>(
3653
signingStep: SigningStep,
3754
msg: {
@@ -106,7 +123,9 @@ export class BabylonBtcStakingManager {
106123
* @param babylonBtcTipHeight - The Babylon BTC tip height.
107124
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
108125
* transaction.
109-
* @param feeRate - The fee rate in satoshis per byte.
126+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
127+
* fee rate is above 1. If the fee rate is too low, the transaction will not
128+
* be included in a block.
110129
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
111130
* @returns The signed babylon pre-staking registration transaction in base64
112131
* format.
@@ -182,7 +201,8 @@ export class BabylonBtcStakingManager {
182201
* @param stakingTxHeight - The BTC height in which the staking transaction
183202
* is included.
184203
* @param stakingInput - The staking inputs.
185-
* @param inclusionProof - The inclusion proof of the staking transaction.
204+
* @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction
205+
* inclusion in a Bitcoin block that is k-deep.
186206
* @param babylonAddress - The Babylon bech32 encoded address of the staker.
187207
* @returns The signed babylon transaction in base64 format.
188208
*/
@@ -249,7 +269,9 @@ export class BabylonBtcStakingManager {
249269
* @param stakingInput - The staking inputs.
250270
* @param inputUTXOs - The UTXOs that will be used to pay for the staking
251271
* transaction.
252-
* @param feeRate - The fee rate in satoshis per byte.
272+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
273+
* fee rate is above 1. If the fee rate is too low, the transaction will not
274+
* be included in a block.
253275
* @returns The estimated BTC fee in satoshis.
254276
*/
255277
estimateBtcStakingFee(
@@ -462,7 +484,9 @@ export class BabylonBtcStakingManager {
462484
* @param stakingParamsVersion - The params version that was used to create the
463485
* delegation in Babylon chain
464486
* @param earlyUnbondingTx - The early unbonding transaction.
465-
* @param feeRate - The fee rate in satoshis per byte.
487+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
488+
* fee rate is above 1. If the fee rate is too low, the transaction will not
489+
* be included in a block.
466490
* @returns The signed withdrawal transaction and its fee.
467491
*/
468492
async createSignedBtcWithdrawEarlyUnbondedTransaction(
@@ -509,7 +533,9 @@ export class BabylonBtcStakingManager {
509533
* @param stakingParamsVersion - The params version that was used to create the
510534
* delegation in Babylon chain
511535
* @param stakingTx - The staking transaction.
512-
* @param feeRate - The fee rate in satoshis per byte.
536+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
537+
* fee rate is above 1. If the fee rate is too low, the transaction will not
538+
* be included in a block.
513539
* @returns The signed withdrawal transaction and its fee.
514540
*/
515541
async createSignedBtcWithdrawStakingExpiredTransaction(
@@ -556,7 +582,9 @@ export class BabylonBtcStakingManager {
556582
* @param stakingParamsVersion - The params version that was used to create the
557583
* delegation in Babylon chain
558584
* @param slashingTx - The slashing transaction.
559-
* @param feeRate - The fee rate in satoshis per byte.
585+
* @param feeRate - The fee rate in satoshis per byte. Typical value for the
586+
* fee rate is above 1. If the fee rate is too low, the transaction will not
587+
* be included in a block.
560588
* @returns The signed withdrawal transaction and its fee.
561589
*/
562590
async createSignedBtcWithdrawSlashingTransaction(
@@ -601,22 +629,42 @@ export class BabylonBtcStakingManager {
601629
*/
602630
async createProofOfPossession(
603631
bech32Address: string,
632+
stakerBtcAddress: string,
604633
): Promise<ProofOfPossessionBTC> {
605-
if (!this.btcProvider.signMessage) {
606-
throw new Error("Sign message function not found");
634+
let sigType: BTCSigType = BTCSigType.ECDSA;
635+
636+
// For Taproot or Native SegWit addresses, use the BIP322 signature scheme
637+
// in the proof of possession as it uses the same signature type as the regular
638+
// input UTXO spend. For legacy addresses, use the ECDSA signature scheme.
639+
if (
640+
isTaproot(stakerBtcAddress, this.network)
641+
|| isNativeSegwit(stakerBtcAddress, this.network)
642+
) {
643+
sigType = BTCSigType.BIP322;
607644
}
608-
// Create Proof of Possession
609-
const bech32AddressHex = uint8ArrayToHex(fromBech32(bech32Address).data);
610-
645+
611646
const signedBabylonAddress = await this.btcProvider.signMessage(
612647
SigningStep.PROOF_OF_POSSESSION,
613-
bech32AddressHex,
614-
"ecdsa",
648+
bech32Address,
649+
sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa",
615650
);
616-
const ecdsaSig = Uint8Array.from(Buffer.from(signedBabylonAddress, "base64"));
651+
652+
let btcSig: Uint8Array;
653+
if (sigType === BTCSigType.BIP322) {
654+
const bip322Sig = BIP322Sig.fromPartial({
655+
address: stakerBtcAddress,
656+
sig: Buffer.from(signedBabylonAddress, "base64"),
657+
});
658+
// Encode the BIP322 protobuf message to a Uint8Array
659+
btcSig = BIP322Sig.encode(bip322Sig).finish();
660+
} else {
661+
// Encode the ECDSA signature to a Uint8Array
662+
btcSig = Buffer.from(signedBabylonAddress, "base64");
663+
}
664+
617665
return {
618-
btcSigType: BTCSigType.ECDSA,
619-
btcSig: ecdsaSig,
666+
btcSigType: sigType,
667+
btcSig
620668
};
621669
}
622670

@@ -710,7 +758,10 @@ export class BabylonBtcStakingManager {
710758
}
711759

712760
// Create proof of possession
713-
const proofOfPossession = await this.createProofOfPossession(bech32Address);
761+
const proofOfPossession = await this.createProofOfPossession(
762+
bech32Address,
763+
stakerBtcInfo.address,
764+
);
714765

715766
// Prepare the final protobuf message
716767
const msg: btcstakingtx.MsgCreateBTCDelegation =

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,19 @@ export class StakingScriptData {
104104
// check that the threshold is above 0 and less than or equal to
105105
// the size of the covenant emulators set
106106
if (
107-
this.covenantThreshold == 0 ||
107+
this.covenantThreshold <= 0 ||
108108
this.covenantThreshold > this.covenantKeys.length
109109
) {
110110
return false;
111111
}
112112

113113
// check that maximum value for staking time is not greater than uint16 and above 0
114-
if (this.stakingTimeLock == 0 || this.stakingTimeLock > 65535) {
114+
if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) {
115115
return false;
116116
}
117117

118118
// check that maximum value for unbonding time is not greater than uint16 and above 0
119-
if (this.unbondingTimeLock == 0 || this.unbondingTimeLock > 65535) {
119+
if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) {
120120
return false;
121121
}
122122

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Psbt, Transaction, networks, payments, script, address } from "bitcoinjs-lib";
1+
import { Psbt, Transaction, networks, payments, script, address, opcodes } from "bitcoinjs-lib";
22
import { Taptree } from "bitcoinjs-lib/src/types";
33

44
import { BTC_DUST_SAT } from "../constants/dustSat";
@@ -15,6 +15,7 @@ import { REDEEM_VERSION } from "../constants/transaction";
1515

1616
// https://bips.xyz/370
1717
const BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 500000000;
18+
const BTC_SLASHING_FRACTION_DIGITS = 4;
1819

1920
/**
2021
* Constructs an unsigned BTC Staking transaction in psbt format.
@@ -537,7 +538,7 @@ function slashingTransaction(
537538
throw new Error("Slashing rate must be between 0 and 1");
538539
}
539540
// Round the slashing rate to two decimal places
540-
slashingRate = parseFloat(slashingRate.toFixed(2));
541+
slashingRate = parseFloat(slashingRate.toFixed(BTC_SLASHING_FRACTION_DIGITS));
541542
// Minimum fee must be a postive integer
542543
if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
543544
throw new Error("Minimum fee must be a positve integer");
@@ -574,9 +575,17 @@ function slashingTransaction(
574575
const stakingAmount = transaction.outs[outputIndex].value;
575576
// Slashing rate is a percentage of the staking amount, rounded down to
576577
// the nearest integer to avoid sending decimal satoshis
577-
const slashingAmount = Math.floor(stakingAmount * slashingRate);
578-
if (slashingAmount <= BTC_DUST_SAT) {
579-
throw new Error("Slashing amount is less than dust limit");
578+
const slashingAmount = Math.round(stakingAmount * slashingRate);
579+
580+
// Compute the slashing output
581+
const slashingOutput = Buffer.from(slashingPkScriptHex, "hex");
582+
583+
// If OP_RETURN is not included, the slashing amount must be greater than the
584+
// dust limit.
585+
if (opcodes.OP_RETURN != slashingOutput[0]) {
586+
if (slashingAmount <= BTC_DUST_SAT) {
587+
throw new Error("Slashing amount is less than dust limit");
588+
}
580589
}
581590

582591
const userFunds = stakingAmount - slashingAmount - minimumFee;
@@ -602,7 +611,7 @@ function slashingTransaction(
602611

603612
// Add the slashing output
604613
psbt.addOutput({
605-
script: Buffer.from(slashingPkScriptHex, "hex"),
614+
script: slashingOutput,
606615
value: slashingAmount,
607616
});
608617

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

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,60 @@ export const isValidBitcoinAddress = (
3232
* @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).
3333
* @returns {boolean} - True if the address is a Taproot address, otherwise false.
3434
*/
35-
export const isTaproot = (taprootAddress: string, network: networks.Network): boolean => {
35+
export const isTaproot = (
36+
taprootAddress: string,
37+
network: networks.Network,
38+
): boolean => {
3639
try {
3740
const decoded = address.fromBech32(taprootAddress);
3841
if (decoded.version !== 1) {
3942
return false;
4043
}
41-
switch (network) {
42-
case networks.bitcoin:
43-
// Check if address statrts with "bc1p"
44-
return taprootAddress.startsWith("bc1p");
45-
case networks.testnet:
46-
// signet, regtest and testnet taproot addresses start with "tb1p" or "sb1p"
47-
return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
48-
default:
49-
return false;
50-
}
44+
45+
// Compare network properties instead of object reference
46+
// The bech32 is hardcoded in the bitcoinjs-lib library.
47+
// https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36
48+
if (network.bech32 === networks.bitcoin.bech32) {
49+
// Check if address starts with "bc1p"
50+
return taprootAddress.startsWith("bc1p");
51+
} else if (network.bech32 === networks.testnet.bech32) {
52+
// signet, regtest and testnet taproot addresses start with "tb1p" or "sb1p"
53+
return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p");
54+
}
55+
return false;
56+
} catch (error) {
57+
return false;
58+
}
59+
};
60+
61+
/**
62+
* Check whether the given address is a Native SegWit address.
63+
*
64+
* @param {string} segwitAddress - The Bitcoin bech32 encoded address to check.
65+
* @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).
66+
* @returns {boolean} - True if the address is a Native SegWit address, otherwise false.
67+
*/
68+
export const isNativeSegwit = (
69+
segwitAddress: string,
70+
network: networks.Network,
71+
): boolean => {
72+
try {
73+
const decoded = address.fromBech32(segwitAddress);
74+
if (decoded.version !== 0) {
75+
return false;
76+
}
77+
78+
// Compare network properties instead of object reference
79+
// The bech32 is hardcoded in the bitcoinjs-lib library.
80+
// https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36
81+
if (network.bech32 === networks.bitcoin.bech32) {
82+
// Check if address starts with "bc1q"
83+
return segwitAddress.startsWith("bc1q");
84+
} else if (network.bech32 === networks.testnet.bech32) {
85+
// testnet native segwit addresses start with "tb1q"
86+
return segwitAddress.startsWith("tb1q");
87+
}
88+
return false;
5189
} catch (error) {
5290
return false;
5391
}

0 commit comments

Comments
 (0)