Skip to content

Commit

Permalink
fix: improvement on slashingTx method (#61)
Browse files Browse the repository at this point in the history
* fix: slashingTx should check outputIndex and slahing rate in range, also add more unit tests for slashing
  • Loading branch information
jrwbabylonlab authored Jul 15, 2024
1 parent 97f530c commit e666242
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 27 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ These are Babylon parameters and should be collected from the Babylon system.
```ts
// The address to which the slashed funds should go to.
const slashingAddress: string = "";
// The slashing percentage rate.
// The slashing percentage rate. It shall be decimal number between 0-1
const slashingRate: number = 0;
// The required fee for the slashing transaction in satoshis.
const minimumSlashingFee: number = 500;
Expand Down
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": "btc-staking-ts",
"version": "0.3.0-canary.5",
"version": "0.3.0-canary.6",
"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
59 changes: 37 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ export function slashTimelockUnbondedTransaction(
* - scripts: Scripts used to construct the taproot output.
* - slashingScript: Script for the slashing condition.
* - unbondingTimelockScript: Script for the unbonding timelock condition.
* - transaction: The original staking transaction.
* - transaction: The unbonding transaction.
* - slashingAddress: The address to send the slashed funds to.
* - slashingRate: The rate at which the funds are slashed (0 < slashingRate < 1).
* - minimumFee: The minimum fee for the transaction in satoshis.
Expand All @@ -442,7 +442,7 @@ export function slashTimelockUnbondedTransaction(
* - psbt: The partially signed transaction (PSBT).
*
* @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript
* @param {Transaction} unbondingTx - The original staking transaction.
* @param {Transaction} unbondingTx - The unbonding transaction.
* @param {string} slashingAddress - The address to send the slashed funds to.
* @param {number} slashingRate - The rate at which the funds are slashed.
* @param {number} minimumSlashingFee - The minimum fee for the transaction in satoshis.
Expand Down Expand Up @@ -497,17 +497,17 @@ export function slashEarlyUnbondedTransaction(
* - scripts: Scripts used to construct the taproot output.
* - slashingScript: Script for the slashing condition.
* - unbondingTimelockScript: Script for the unbonding timelock condition.
* - transaction: The original staking transaction.
* - transaction: The original staking/unbonding transaction.
* - slashingAddress: The address to send the slashed funds to.
* - slashingRate: The rate at which the funds are slashed (0 < slashingRate < 1).
* - minimumFee: The minimum fee for the transaction in satoshis.
* - network: The Bitcoin network.
* - outputIndex: The index of the output to be spent in the original transaction (default is 0).
*
* @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript
* @param {Transaction} transaction - The original staking transaction.
* @param {Transaction} transaction - The original staking/unbonding transaction.
* @param {string} slashingAddress - The address to send the slashed funds to.
* @param {number} slashingRate - The rate at which the funds are slashed.
* @param {number} slashingRate - The rate at which the funds are slashed. Two decimal places, otherwise it will be rounded down.
* @param {number} minimumFee - The minimum fee for the transaction in satoshis.
* @param {networks.Network} network - The Bitcoin network.
* @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction.
Expand All @@ -529,13 +529,24 @@ function slashingTransaction(
psbt: Psbt;
} {
// Check that slashing rate and minimum fee are bigger than 0
if (slashingRate <= 0 || minimumFee <= 0) {
throw new Error("Slashing rate and minimum fee must be bigger than 0");
if (slashingRate <= 0 || slashingRate >= 1) {
throw new Error("Slashing rate must be between 0 and 1");
}
// Round the slashing rate to two decimal places
slashingRate = parseFloat(slashingRate.toFixed(2));
// Minimum fee must be a postive integer
if (minimumFee <= 0 || !Number.isInteger(minimumFee)) {
throw new Error("Minimum fee must be a positve integer");
}

// Check that outputIndex is bigger or equal to 0
if (outputIndex < 0) {
throw new Error("Output index must be bigger or equal to 0");
if (outputIndex < 0 || !Number.isInteger(outputIndex)) {
throw new Error("Output index must be an integer bigger or equal to 0");
}

// Check that outputIndex is within the bounds of the transaction
if (!transaction.outs[outputIndex]) {
throw new Error("Output index is out of range");
}

const redeem = {
Expand All @@ -556,31 +567,37 @@ function slashingTransaction(
controlBlock: p2tr.witness![p2tr.witness!.length - 1],
};

const stakingAmount = transaction.outs[outputIndex].value;
// Slashing rate is a percentage of the staking amount, rounded down to
// the nearest integer to avoid sending decimal satoshis
const slashingAmount = Math.floor(stakingAmount * slashingRate);
if (slashingAmount <= BTC_DUST_SAT) {
throw new Error("Slashing amount is less than dust limit");
}

const userFunds = stakingAmount - slashingAmount - minimumFee;
if (userFunds <= BTC_DUST_SAT) {
throw new Error("User funds are less than dust limit");
}


const psbt = new Psbt({ network });
psbt.addInput({
hash: transaction.getHash(),
index: outputIndex,
tapInternalKey: internalPubkey,
witnessUtxo: {
value: transaction.outs[outputIndex].value,
value: stakingAmount,
script: transaction.outs[outputIndex].script,
},
tapLeafScript: [tapLeafScript],
});

const userValue =
transaction.outs[outputIndex].value * (1 - slashingRate) - minimumFee;

// We need to verify that this is above 0
if (userValue <= 0) {
// If it is not, then an error is thrown and the user has to stake more
throw new Error("Not enough funds to slash, stake more");
}

// Add the slashing output
psbt.addOutput({
address: slashingAddress,
value: transaction.outs[outputIndex].value * slashingRate,
value: slashingAmount,
});

// Change output contains unbonding timelock script
Expand All @@ -589,12 +606,10 @@ function slashingTransaction(
scriptTree: { output: scripts.unbondingTimelockScript },
network,
});

// Add the change output
psbt.addOutput({
address: changeOutput.address!,
value:
transaction.outs[outputIndex].value * (1 - slashingRate) - minimumFee,
value: userFunds,
});

return { psbt };
Expand Down
5 changes: 5 additions & 0 deletions tests/helper/dataGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export class DataGenerator {
return Math.floor(Math.random() * 1000) + 1;
};

// Real values will likely be in range 0.01 to 0.30
generateRandomSlashingRate(min: number = 0.01, max: number = 0.30): number {
return parseFloat((Math.random() * (max - min) + min).toFixed(2));
}

// Convenant committee are a list of public keys that are used to sign a covenant
generateRandomCovenantCommittee = (size: number): Buffer[] => {
const committe: Buffer[] = [];
Expand Down
Loading

0 comments on commit e666242

Please sign in to comment.