Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support slashing & simply data structure for phase 2 #37

Merged
merged 6 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@babylonlabs-io/btc-staking-ts",
"version": "0.4.0-canary.1",
"version": "0.4.0-canary.2",
"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 Down
171 changes: 162 additions & 9 deletions src/staking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { UTXO } from "../types/UTXO";
import { StakingScriptData, StakingScripts } from "./stakingScript";
import { StakingError, StakingErrorCode } from "../error";
import {
slashEarlyUnbondedTransaction,
slashTimelockUnbondedTransaction,
stakingTransaction, unbondingTransaction,
withdrawEarlyUnbondedTransaction,
withdrawTimelockUnbondedTransaction
} from "./transactions";
import {
deriveAddressFromPkScript,
isTaproot,
isValidBitcoinAddress, isValidNoCoordPublicKey
} from "../utils/btc";
Expand Down Expand Up @@ -176,6 +179,32 @@ export class Staking {
"Covenant quorum must be greater than 0",
);
}
if (params.slashing) {
if (params.slashing.slashingRate <= 0) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Slashing rate must be greater than 0",
);
}
if (params.slashing.slashingRate > 1) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Slashing rate must be less or equal to 1",
);
}
if (params.slashing.slashingPkScript.length == 0) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Slashing public key script is missing",
);
}
if (params.slashing.minSlashingTxFeeSat <= 0) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Minimum slashing transaction fee must be greater than 0",
);
}
}
}

/**
Expand Down Expand Up @@ -246,7 +275,7 @@ export class Staking {
*
* @returns {PsbtTransactionResult} - An object containing the unsigned psbt and fee
*/
public createStakingTransaction (
public createStakingTransaction(
params: StakingParams,
stakingAmountSat: number,
timelock: number,
Expand Down Expand Up @@ -290,19 +319,19 @@ export class Staking {
};

/**
* Create an unbonding transaction for observable staking.
* Create an unbonding transaction for staking.
*
* @param {ObservableStakingParams} stakingParams - The staking parameters for observable staking.
* @param {ObservableStakingParams} stakingParams - The staking parameters for staking.
* @param {Delegation} delegation - The delegation to unbond.
*
* @returns {Psbt} - The unsigned unbonding transaction
*
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
public createUnbondingTransaction = (
public createUnbondingTransaction(
stakingParams: StakingParams,
delegation: Delegation,
) : PsbtTransactionResult => {
) : PsbtTransactionResult {
this.validateParams(stakingParams);
this.validateDelegationInputs(
delegation, stakingParams, this.stakerInfo,
Expand Down Expand Up @@ -351,12 +380,12 @@ export class Staking {
*
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
public createWithdrawEarlyUnbondedTransaction = (
public createWithdrawEarlyUnbondedTransaction (
stakingParams: StakingParams,
delegation: Delegation,
unbondingTx: Transaction,
feeRate: number,
): PsbtTransactionResult => {
): PsbtTransactionResult {
this.validateParams(stakingParams);
this.validateDelegationInputs(
delegation, stakingParams, this.stakerInfo,
Expand Down Expand Up @@ -402,11 +431,11 @@ export class Staking {
*
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
public createWithdrawTimelockUnbondedTransaction = (
public createWithdrawTimelockUnbondedTransaction(
stakingParams: StakingParams,
delegation: Delegation,
feeRate: number,
): PsbtTransactionResult => {
): PsbtTransactionResult {
this.validateParams(stakingParams);
this.validateDelegationInputs(
delegation, stakingParams, this.stakerInfo,
Expand Down Expand Up @@ -444,4 +473,128 @@ export class Staking {
);
}
}

/**
* Create a slash timelock unbonded transaction for staking.
*
* @param {StakingParams} stakingParams - The staking parameters for staking.
* @param {Delegation} delegation - The delegation to slash.
*
* @returns {PsbtTransactionResult} - An object containing the unsigned psbt and fee
*
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
public createSlashTimelockUnbondedTransaction(
jrwbabylonlab marked this conversation as resolved.
Show resolved Hide resolved
stakingParams: StakingParams,
delegation: Delegation,
) : PsbtTransactionResult {
this.validateParams(stakingParams);
if (!stakingParams.slashing) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Slashing parameters are missing",
);
}
this.validateDelegationInputs(
delegation, stakingParams, this.stakerInfo,
);
const slashingAddress = deriveAddressFromPkScript(
stakingParams.slashing.slashingPkScript, this.network,
);
const {
stakingTx,
timelock,
finalityProviderPkNoCoordHex,
stakerPkNoCoordHex,
} = delegation;

// Build scripts
const scripts = this.buildScripts(
stakingParams,
finalityProviderPkNoCoordHex,
timelock,
stakerPkNoCoordHex,
);

// create the slash timelock unbonded transaction
try {
const { psbt } = slashTimelockUnbondedTransaction(
scripts,
stakingTx,
slashingAddress,
stakingParams.slashing.slashingRate,
stakingParams.slashing.minSlashingTxFeeSat,
this.network,
);
return { psbt, fee: stakingParams.slashing.minSlashingTxFeeSat };
} catch (error) {
throw StakingError.fromUnknown(
error, StakingErrorCode.BUILD_TRANSACTION_FAILURE,
"Cannot build the slash timelock unbonded transaction",
);
}
}

/**
* Create a slash early unbonded transaction for staking.
*
* @param {StakingParams} stakingParams - The staking parameters for staking.
* @param {Delegation} delegation - The delegation to slash.
* @param {Transaction} unbondingTx - The unbonding transaction to slash.
*
* @returns {PsbtTransactionResult} - An object containing the unsigned psbt and fee
*
* @throws {StakingError} - If the delegation is invalid or the transaction cannot be built
*/
public createSlashEarlyUnbondedTransaction(
stakingParams: StakingParams,
delegation: Delegation,
unbondingTx: Transaction,
): PsbtTransactionResult {
this.validateParams(stakingParams);
if (!stakingParams.slashing) {
throw new StakingError(
StakingErrorCode.INVALID_PARAMS,
"Slashing parameters are missing",
);
}
this.validateDelegationInputs(
delegation, stakingParams, this.stakerInfo,
);
const slashingAddress = deriveAddressFromPkScript(
jrwbabylonlab marked this conversation as resolved.
Show resolved Hide resolved
stakingParams.slashing.slashingPkScript, this.network,
);

const {
timelock,
finalityProviderPkNoCoordHex,
stakerPkNoCoordHex,
} = delegation;

// Build scripts
const scripts = this.buildScripts(
stakingParams,
finalityProviderPkNoCoordHex,
timelock,
stakerPkNoCoordHex,
);

// create the slash timelock unbonded transaction
try {
const { psbt } = slashEarlyUnbondedTransaction(
scripts,
unbondingTx,
slashingAddress,
stakingParams.slashing.slashingRate,
stakingParams.slashing.minSlashingTxFeeSat,
this.network,
);
return { psbt, fee: stakingParams.slashing.minSlashingTxFeeSat };
} catch (error) {
throw StakingError.fromUnknown(
error, StakingErrorCode.BUILD_TRANSACTION_FAILURE,
"Cannot build the slash early unbonded transaction",
);
}
}
}
2 changes: 1 addition & 1 deletion src/staking/observable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class ObservableStaking extends Staking {
*
* @returns {PsbtTransactionResult} - An object containing the unsigned psbt and fee
*/
public createStakingTransaction (
public createStakingTransaction(
params: ObservableStakingParams,
stakingAmountSat: number,
timelock: number,
Expand Down
1 change: 1 addition & 0 deletions src/types/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface StakingParams {
slashing?: {
slashingPkScript: string;
slashingRate: number;
minSlashingTxFeeSat: number;
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/utils/btc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,22 @@ const validateNoCoordPublicKeyBuffer = (pkBuffer: Buffer): boolean => {
return (
ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd)
);
};

/**
* Derive the Bitcoin address from the public key script.
*
* @param {string} scriptPubKey - The public key script in hex.
* @param {networks.Network} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).
*
* @returns {string} - The Bitcoin address.
* @throws {Error} - If the address cannot be derived from the public key script.
*/
export const deriveAddressFromPkScript = (scriptPubKey: string, network: networks.Network): string => {
jrwbabylonlab marked this conversation as resolved.
Show resolved Hide resolved
try {
const scriptPubKeyBuffer = Buffer.from(scriptPubKey, "hex");
return address.fromOutputScript(scriptPubKeyBuffer, network);
} catch (error) {
throw new Error(`Failed to derive address from public key script: ${error}`);
}
};
29 changes: 29 additions & 0 deletions tests/helper/datagen/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export class StakingDataGenerator {
const maxStakingTimeBlocks = fixedTerm ? minStakingTimeBlocks : this.getRandomIntegerBetween(minStakingTimeBlocks, minStakingTimeBlocks + 1000);
const timelock = this.generateRandomTimelock({minStakingTimeBlocks, maxStakingTimeBlocks});
const unbondingTime = this.generateRandomUnbondingTime(timelock);
const slashingRate = this.generateRandomSlashingRate();
const minSlashingTxFeeSat = this.getRandomIntegerBetween(1000, 100000);
return {
covenantNoCoordPks,
covenantQuorum,
Expand All @@ -53,6 +55,11 @@ export class StakingDataGenerator {
),
minStakingTimeBlocks,
maxStakingTimeBlocks,
slashing: {
slashingRate,
slashingPkScript: getRandomPaymentScriptHex(this.generateRandomKeyPair().publicKey),
minSlashingTxFeeSat,
}
};
}

Expand Down Expand Up @@ -309,3 +316,25 @@ export class StakingDataGenerator {
};
};
}

function getRandomPaymentScriptHex(pubKeyHex: string): string {
const pubKeyBuf = Buffer.from(pubKeyHex, "hex");

// Define the possible payment types
const paymentTypes = [
bitcoin.payments.p2pkh({ pubkey: pubKeyBuf }),
bitcoin.payments.p2sh({ redeem: bitcoin.payments.p2wpkh({ pubkey: pubKeyBuf }) }),
bitcoin.payments.p2wpkh({ pubkey: pubKeyBuf }),
];

// Randomly pick one payment type
const randomIndex = Math.floor(Math.random() * paymentTypes.length);
const payment = paymentTypes[randomIndex];

// Get the scriptPubKey from the selected payment type and return its hex representation
if (!payment.output) {
throw new Error("Failed to generate scriptPubKey.");
}

return payment.output.toString("hex");
}
Loading