Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/babylonlabs-io-btc-staking-ts/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,4 @@ $RECYCLE.BIN/
*.swp
*.swo

build/
build/
3 changes: 1 addition & 2 deletions modules/babylonlabs-io-btc-staking-ts/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
const { initBTCCurve } = require("./src");

const originalTest = global.test;
const NUM_ITERATIONS = 3;
;
const NUM_ITERATIONS = parseInt(process.env.TEST_REPEAT_TIMES) || 1;

initBTCCurve();

Expand Down
6 changes: 3 additions & 3 deletions modules/babylonlabs-io-btc-staking-ts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bitgo/babylonlabs-io-btc-staking-ts",
"version": "2.4.1",
"version": "3.0.0",
"description": "Library exposing methods for the creation and consumption of Bitcoin transactions pertaining to Babylon's Bitcoin Staking protocol.",
"module": "dist/index.js",
"main": "dist/index.cjs",
Expand All @@ -27,7 +27,7 @@
"btc-staking"
],
"engines": {
"node": ">=20 < 23"
"node": ">=18 < 23"
},
"author": "Babylon Labs Ltd.",
"license": "SEE LICENSE IN LICENSE",
Expand All @@ -37,7 +37,7 @@
"nanoevents": "^9.1.0"
},
"dependencies": {
"@babylonlabs-io/babylon-proto-ts": "1.0.0",
"@babylonlabs-io/babylon-proto-ts": "1.7.2",
"@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3",
"@cosmjs/encoding": "^0.33.0",
"bip174": "=2.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
export const DEFAULT_INPUT_SIZE = 180;
// Estimated size of a P2WPKH input in bytes
export const P2WPKH_INPUT_SIZE = 68;
// Estimated size of a P2TR input in bytes
// Estimated size of a P2TR input in bytes. 42vb inputs + 16vb witness
export const P2TR_INPUT_SIZE = 58;
// Estimated size of a P2TR input in bytes for staking expansion transactions.
// This value accounts for the witness size including covenant signatures
// and is calibrated for a typical covenant quorum of 6 signatures.
export const P2TR_STAKING_EXPANSION_INPUT_SIZE = 268;
// Estimated size of a transaction buffer in bytes
export const TX_BUFFER_SIZE_OVERHEAD = 11;
// Buffer for estimation accuracy when fee rate <= 2 sat/byte
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const BABYLON_REGISTRY_TYPE_URLS = {
MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation",
MsgBtcStakeExpand: "/babylon.btcstaking.v1.MsgBtcStakeExpand",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Staking module address for the Babylon Genesis chain.
* This address is derived deterministically from the module name and is the same across all environments.
*/
export const STAKING_MODULE_ADDRESS = "bbn13837feaxn8t0zvwcjwhw7lhpgdcx4s36eqteah";
177 changes: 173 additions & 4 deletions modules/babylonlabs-io-btc-staking-ts/src/staking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import {
deriveStakingOutputInfo,
findMatchingTxOutputIndex,
toBuffers,
validateParams,
validateStakingTimelock,
validateStakingTxInputData,
} from "../utils/staking";
import { stakingPsbt, unbondingPsbt } from "./psbt";
import { stakingExpansionPsbt, stakingPsbt, unbondingPsbt } from "./psbt";
import { StakingScriptData, StakingScripts } from "./stakingScript";
import {
stakingExpansionTransaction,
slashEarlyUnbondedTransaction,
slashTimelockUnbondedTransaction,
stakingTransaction,
Expand All @@ -28,6 +26,7 @@ import {
withdrawSlashingTransaction,
withdrawTimelockUnbondedTransaction,
} from "./transactions";
import { validateParams, validateStakingExpansionCovenantQuorum, validateStakingTimelock, validateStakingTxInputData } from "../utils/staking/validation";
export * from "./stakingScript";

export interface StakerInfo {
Expand Down Expand Up @@ -171,6 +170,106 @@ export class Staking {
}
}

/**
* Creates a staking expansion transaction that extends an existing BTC stake
* to new finality providers or renews the timelock.
*
* This method implements RFC 037 BTC Stake Expansion,
* allowing existing active BTC staking transactions
* to extend their delegation to new finality providers without going through
* the full unbonding process.
*
* The expansion transaction:
* 1. Spends the previous staking transaction output as the first input
* 2. Uses funding UTXO as additional input to cover transaction fees or
* to increase the staking amount
* 3. Creates a new staking output with expanded finality provider coverage or
* renews the timelock
* 4. Has an output returning the remaining funds as change (if any) to the
* staker BTC address
*
* @param {number} stakingAmountSat - The total staking amount in satoshis
* (The amount had to be equal to the previous staking amount for now, this
* lib does not yet support increasing the staking amount at this stage)
* @param {UTXO[]} inputUTXOs - Available UTXOs to use for funding the
* expansion transaction fees. Only one will be selected for the expansion
* @param {number} feeRate - Fee rate in satoshis per byte for the
* expansion transaction
* @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
* used in the previous staking transaction
* @param {Object} previousStakingTxInfo - Necessary information to spend the
* previous staking transaction.
* @returns {TransactionResult & { fundingUTXO: UTXO }} - An object containing
* the unsigned expansion transaction and calculated fee, and the funding UTXO
* @throws {StakingError} - If the transaction cannot be built or validation
* fails
*/
public createStakingExpansionTransaction(
stakingAmountSat: number,
inputUTXOs: UTXO[],
feeRate: number,
paramsForPreviousStakingTx: StakingParams,
previousStakingTxInfo: {
stakingTx: Transaction,
stakingInput: {
finalityProviderPksNoCoordHex: string[],
stakingTimelock: number,
},
},
): TransactionResult & {
fundingUTXO: UTXO;
} {
validateStakingTxInputData(
stakingAmountSat,
this.stakingTimelock,
this.params,
inputUTXOs,
feeRate,
);
validateStakingExpansionCovenantQuorum(
paramsForPreviousStakingTx,
this.params,
);

// Create a Staking instance for the previous staking transaction
// This allows us to build the scripts needed to spend the previous
// staking output
const previousStaking = new Staking(
this.network,
this.stakerInfo,
paramsForPreviousStakingTx,
previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
previousStakingTxInfo.stakingInput.stakingTimelock,
);

// Build the expansion transaction using the stakingExpansionTransaction
// utility function.
// This creates a transaction that spends the previous staking output and
// creates new staking outputs
const {
transaction: stakingExpansionTx,
fee: stakingExpansionTxFee,
fundingUTXO,
} = stakingExpansionTransaction(
this.network,
this.buildScripts(),
stakingAmountSat,
this.stakerInfo.address,
feeRate,
inputUTXOs,
{
stakingTx: previousStakingTxInfo.stakingTx,
scripts: previousStaking.buildScripts(),
},
)

return {
transaction: stakingExpansionTx,
fee: stakingExpansionTxFee,
fundingUTXO,
};
}

/**
* Create a staking psbt based on the existing staking transaction.
*
Expand Down Expand Up @@ -200,6 +299,76 @@ export class Staking {
);
}

/**
* Convert a staking expansion transaction to a PSBT.
*
* @param {Transaction} stakingExpansionTx - The staking expansion
* transaction to convert
* @param {UTXO[]} inputUTXOs - Available UTXOs for the
* funding input (second input)
* @param {StakingParams} paramsForPreviousStakingTx - Staking parameters
* used for the previous staking transaction
* @param {Object} previousStakingTxInfo - Information about the previous
* staking transaction
* @returns {Psbt} The PSBT for the staking expansion transaction
* @throws {Error} If the previous staking output cannot be found or
* validation fails
*/
public toStakingExpansionPsbt(
stakingExpansionTx: Transaction,
inputUTXOs: UTXO[],
paramsForPreviousStakingTx: StakingParams,
previousStakingTxInfo: {
stakingTx: Transaction,
stakingInput: {
finalityProviderPksNoCoordHex: string[],
stakingTimelock: number,
},
},
): Psbt {
// Reconstruct the previous staking instance to access its scripts and
// parameters. This is necessary because we need to identify which output
// in the previous staking transaction is the staking output (it could be
// at any output index)
const previousStaking = new Staking(
this.network,
this.stakerInfo,
paramsForPreviousStakingTx,
previousStakingTxInfo.stakingInput.finalityProviderPksNoCoordHex,
previousStakingTxInfo.stakingInput.stakingTimelock,
);

// Find the staking output address in the previous staking transaction
const previousScripts = previousStaking.buildScripts();
const { outputAddress } = deriveStakingOutputInfo(previousScripts, this.network);

// Find the output index in the previous staking transaction that matches
// the staking output address.
const previousStakingOutputIndex = findMatchingTxOutputIndex(
previousStakingTxInfo.stakingTx,
outputAddress,
this.network,
);

// Create and return the PSBT for the staking expansion transaction
// The PSBT will have two inputs:
// 1. The previous staking output
// 2. A funding UTXO from inputUTXOs (for additional funds)
return stakingExpansionPsbt(
this.network,
stakingExpansionTx,
{
stakingTx: previousStakingTxInfo.stakingTx,
outputIndex: previousStakingOutputIndex,
},
inputUTXOs,
previousScripts,
isTaproot(this.stakerInfo.address, this.network)
? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex")
: undefined,
);
}

/**
* Create an unbonding transaction for staking.
*
Expand Down
Loading