From 8090e00b7fd394914fd854fe58927083d2d427d5 Mon Sep 17 00:00:00 2001 From: Ben Kelcher Date: Thu, 2 Oct 2025 08:21:59 -0400 Subject: [PATCH 01/10] chore: vendor babylonlabs-io/btc-staking-ts#v2.5.7 This commit was generated by the vendor-github-repo script. Ticket: SC-3362 --- .../.eslintrc.json | 31 + .../.github/workflows/ci.yml | 14 + .../.github/workflows/manual-publish.yaml | 20 + .../.github/workflows/publish.yaml | 18 + .../babylonlabs-io-btc-staking-ts/.gitignore | 2 - .../.husky/commit-msg | 7 + .../.husky/pre-commit | 2 + modules/babylonlabs-io-btc-staking-ts/.npmrc | 1 + modules/babylonlabs-io-btc-staking-ts/.nvmrc | 1 + .../.prettierignore | 10 + .../.prettierrc.json | 9 + .../.releaserc.json | 11 + .../babylonlabs-io-btc-staking-ts/README.md | 58 +- .../build/src/constants/dustSat.d.ts | 1 + .../build/src/constants/dustSat.js | 4 + .../build/src/constants/fee.d.ts | 10 + .../build/src/constants/fee.js | 24 + .../build/src/constants/internalPubkey.d.ts | 1 + .../build/src/constants/internalPubkey.js | 6 + .../build/src/constants/keys.d.ts | 1 + .../build/src/constants/keys.js | 5 + .../build/src/constants/psbt.d.ts | 3 + .../build/src/constants/psbt.js | 9 + .../build/src/constants/registry.d.ts | 3 + .../build/src/constants/registry.js | 6 + .../build/src/constants/transaction.d.ts | 1 + .../build/src/constants/transaction.js | 4 + .../build/src/constants/unbonding.d.ts | 1 + .../build/src/constants/unbonding.js | 6 + .../build/src/error/index.d.ts | 13 + .../build/src/error/index.js | 29 + .../build/src/index.d.ts | 13 + .../build/src/index.js | 35 ++ .../build/src/staking/index.d.ts | 117 ++++ .../build/src/staking/index.js | 274 +++++++++ .../build/src/staking/manager.d.ts | 246 ++++++++ .../build/src/staking/manager.js | 524 +++++++++++++++++ .../build/src/staking/observable/index.d.ts | 53 ++ .../build/src/staking/observable/index.js | 121 ++++ .../observable/observableStakingScript.d.ts | 26 + .../observable/observableStakingScript.js | 60 ++ .../build/src/staking/psbt.d.ts | 20 + .../build/src/staking/psbt.js | 113 ++++ .../build/src/staking/stakingScript.d.ts | 96 +++ .../build/src/staking/stakingScript.js | 257 ++++++++ .../build/src/staking/transactions.d.ts | 191 ++++++ .../build/src/staking/transactions.js | 515 ++++++++++++++++ .../build/src/types/UTXO.d.ts | 9 + .../build/src/types/UTXO.js | 2 + .../build/src/types/covenantSignatures.d.ts | 4 + .../build/src/types/covenantSignatures.js | 2 + .../build/src/types/index.d.ts | 3 + .../build/src/types/index.js | 19 + .../build/src/types/params.d.ts | 34 ++ .../build/src/types/params.js | 2 + .../build/src/types/psbtOutputs.d.ts | 16 + .../build/src/types/psbtOutputs.js | 7 + .../build/src/types/transaction.d.ts | 15 + .../build/src/types/transaction.js | 2 + .../build/src/utils/babylon.d.ts | 7 + .../build/src/utils/babylon.js | 20 + .../build/src/utils/btc.d.ts | 48 ++ .../build/src/utils/btc.js | 179 ++++++ .../build/src/utils/fee/index.d.ts | 33 ++ .../build/src/utils/fee/index.js | 137 +++++ .../build/src/utils/fee/utils.d.ts | 23 + .../build/src/utils/fee/utils.js | 66 +++ .../build/src/utils/index.d.ts | 12 + .../build/src/utils/index.js | 31 + .../build/src/utils/staking/index.d.ts | 116 ++++ .../build/src/utils/staking/index.js | 256 ++++++++ .../build/src/utils/staking/param.d.ts | 3 + .../build/src/utils/staking/param.js | 32 + .../build/src/utils/utxo/findInputUTXO.d.ts | 3 + .../build/src/utils/utxo/findInputUTXO.js | 14 + .../src/utils/utxo/getPsbtInputFields.d.ts | 13 + .../src/utils/utxo/getPsbtInputFields.js | 67 +++ .../build/src/utils/utxo/getScriptType.d.ts | 21 + .../build/src/utils/utxo/getScriptType.js | 59 ++ .../docs/advanced-btc-tx.md | 491 ++++++++++++++++ .../docs/usage.md | 517 +++++++++++++++++ .../jest.setup.js | 3 +- .../package.json | 62 +- .../src/constants/fee.ts | 6 +- .../src/constants/registry.ts | 1 + .../src/constants/staking.ts | 5 + .../src/index.ts | 4 - .../src/staking/index.ts | 177 +++++- .../src/staking/manager.ts | 549 ++++++++++++++---- .../src/staking/observable/index.ts | 3 +- .../src/staking/psbt.ts | 141 +++++ .../src/staking/transactions.ts | 173 +++++- .../src/types/events.ts | 1 + .../src/types/manager.ts | 45 ++ .../src/utils/fee/index.ts | 94 +++ .../src/utils/pop.ts | 59 ++ .../src/utils/staking/index.ts | 225 ++----- .../src/utils/staking/validation.ts | 280 +++++++++ .../tests/helper/datagen/base.ts | 488 ++++++++++++++++ .../tests/helper/datagen/observable.ts | 43 ++ .../tests/helper/index.ts | 2 + .../tests/helper/math.ts | 30 + .../tests/helper/testingNetworks.ts | 30 + .../staking/createCovenantWitness.test.ts | 169 ++++++ .../tests/staking/createSlashingTx.test.ts | 145 +++++ .../tests/staking/createStakingTx.test.ts | 248 ++++++++ .../tests/staking/createUnbondingtx.test.ts | 102 ++++ .../tests/staking/createWithdrawTx.test.ts | 112 ++++ .../tests/staking/manager/__mock__/fee.ts | 233 ++++++++ .../staking/manager/__mock__/providers.ts | 13 + .../staking/manager/__mock__/registration.ts | 494 ++++++++++++++++ .../tests/staking/manager/__mock__/staking.ts | 114 ++++ .../staking/manager/__mock__/unbonding.ts | 145 +++++ .../staking/manager/__mock__/withdrawal.ts | 129 ++++ .../tests/staking/manager/fee.test.ts | 81 +++ .../tests/staking/manager/init.test.ts | 33 ++ .../tests/staking/manager/pop.test.ts | 226 +++++++ .../tests/staking/manager/postStaking.test.ts | 211 +++++++ .../tests/staking/manager/preStaking.test.ts | 228 ++++++++ .../tests/staking/manager/staking.test.ts | 112 ++++ .../tests/staking/manager/unbonding.test.ts | 172 ++++++ .../tests/staking/manager/withdrawal.test.ts | 208 +++++++ .../observable/createStakingTx.test.ts | 162 ++++++ .../observableStakingScript.test.ts | 152 +++++ .../staking/observable/validation.test.ts | 78 +++ .../staking/psbt/stakingExpansionPsbt.test.ts | 464 +++++++++++++++ .../tests/staking/psbt/stakingPsbt.test.ts | 183 ++++++ .../tests/staking/psbt/unbondingPsbt.test.ts | 119 ++++ .../tests/staking/stakingScript.test.ts | 415 +++++++++++++ .../transactions/slashingTransaction.test.ts | 360 ++++++++++++ .../stakingExpansionTransaction.test.ts | 200 +++++++ .../transactions/stakingTransaction.test.ts | 381 ++++++++++++ .../transactions/unbondingTransaction.test.ts | 86 +++ .../transactions/withdrawTransaction.test.ts | 326 +++++++++++ .../tests/staking/validation.test.ts | 367 ++++++++++++ .../tests/utils/btc.test.ts | 213 +++++++ .../utils/fee/stakingExpansionTxFee.test.ts | 276 +++++++++ .../tests/utils/fee/stakingtxFee.test.ts | 242 ++++++++ .../tests/utils/fee/utils.test.ts | 128 ++++ .../tests/utils/fee/withdrawTxFee.test.ts | 26 + .../tests/utils/pop.test.ts | 376 ++++++++++++ .../staking/findMatchingTxOutputIndex.test.ts | 90 +++ .../tests/utils/staking/validation.test.ts | 297 ++++++++++ .../utils/utxo/getPsbtInputFields.test.ts | 259 +++++++++ 144 files changed, 15723 insertions(+), 308 deletions(-) create mode 100644 modules/babylonlabs-io-btc-staking-ts/.eslintrc.json create mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml create mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml create mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml create mode 100644 modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg create mode 100644 modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit create mode 100644 modules/babylonlabs-io-btc-staking-ts/.npmrc create mode 100644 modules/babylonlabs-io-btc-staking-ts/.nvmrc create mode 100644 modules/babylonlabs-io-btc-staking-ts/.prettierignore create mode 100644 modules/babylonlabs-io-btc-staking-ts/.prettierrc.json create mode 100644 modules/babylonlabs-io-btc-staking-ts/.releaserc.json create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js create mode 100644 modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md create mode 100644 modules/babylonlabs-io-btc-staking-ts/docs/usage.md create mode 100644 modules/babylonlabs-io-btc-staking-ts/src/constants/staking.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/src/utils/pop.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/src/utils/staking/validation.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts create mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts diff --git a/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json b/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json new file mode 100644 index 0000000000..943cb53e4d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json @@ -0,0 +1,31 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "parserOptions": { + "sourceType": "module", + "ecmaVersion": "latest" + }, + "overrides": [ + { + "files": [ + "tests/**/*.ts" + ], + "parser": "@typescript-eslint/parser", + "rules": { + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-explicit-any": "off" + } + }, + { + "files": [ + "jest.setup.js" + ], + "rules": { + "no-undef": "off" + } + }, + ] +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml new file mode 100644 index 0000000000..d1dd9bdbb7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml @@ -0,0 +1,14 @@ +name: ci + +on: + pull_request: + branches: + - "**" + +jobs: + lint_test: + uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.9.0 + with: + run-build: true + run-unit-tests: true + node-version: 24.2.0 \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml new file mode 100644 index 0000000000..9b95a3c66d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml @@ -0,0 +1,20 @@ +name: Manual release branch release + +on: + workflow_dispatch: + push: + branches: + - 'release/v[0-9]+.[0-9]+.[0-9]+' +permissions: + contents: write +jobs: + lint_test: + uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.13.1 + secrets: inherit + with: + run-build: true + run-unit-tests: true + publish: true + publish-command: | + ./bin/ci_validate_version.sh + npm publish diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml new file mode 100644 index 0000000000..88a5643ed3 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml @@ -0,0 +1,18 @@ +name: Semantic release + +on: + workflow_dispatch: + push: + branches: + - main +permissions: + contents: write +jobs: + lint_test: + uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.13.1 + secrets: inherit + with: + run-build: true + run-unit-tests: true + use-semantic-release: true + node-version: 24.2.0 diff --git a/modules/babylonlabs-io-btc-staking-ts/.gitignore b/modules/babylonlabs-io-btc-staking-ts/.gitignore index 6b0417d7d6..2f621f05c0 100644 --- a/modules/babylonlabs-io-btc-staking-ts/.gitignore +++ b/modules/babylonlabs-io-btc-staking-ts/.gitignore @@ -206,5 +206,3 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/node,macos,windows *.swp *.swo - -build/ \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg b/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg new file mode 100644 index 0000000000..3629fd94ab --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg @@ -0,0 +1,7 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +# Only run commitlint if a commit message file was passed in +if [ -n "$1" ] && [ -f "$1" ]; then + npx --no-install commitlint --edit "$1" +fi \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit b/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit new file mode 100644 index 0000000000..045cbbdb56 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit @@ -0,0 +1,2 @@ +TEST_REPEAT_TIMES=5 npm test +npm run build \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.npmrc b/modules/babylonlabs-io-btc-staking-ts/.npmrc new file mode 100644 index 0000000000..4fd021952d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.nvmrc b/modules/babylonlabs-io-btc-staking-ts/.nvmrc new file mode 100644 index 0000000000..4bbfbca25c --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.nvmrc @@ -0,0 +1 @@ +v24.2.0 \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.prettierignore b/modules/babylonlabs-io-btc-staking-ts/.prettierignore new file mode 100644 index 0000000000..c16757f2e0 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.prettierignore @@ -0,0 +1,10 @@ +node_modules +dist +build +coverage +.circleci +.husky +.next +docs +public +README.md \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json b/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json new file mode 100644 index 0000000000..a07eac9270 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "plugins": [ + "prettier-plugin-organize-imports" + ] +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.releaserc.json b/modules/babylonlabs-io-btc-staking-ts/.releaserc.json new file mode 100644 index 0000000000..2a963bf561 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/.releaserc.json @@ -0,0 +1,11 @@ +{ + "branches": [ + "main" + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/npm", + "@semantic-release/github" + ] +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/README.md b/modules/babylonlabs-io-btc-staking-ts/README.md index 6766b67a62..5d5c743a7f 100644 --- a/modules/babylonlabs-io-btc-staking-ts/README.md +++ b/modules/babylonlabs-io-btc-staking-ts/README.md @@ -1 +1,57 @@ -BitGo Fork of https://github.com/babylonlabs-io/btc-staking-ts/tree/v2.3.4 \ No newline at end of file +

+ Babylon Logo +

@babylonlabs-io/btc-staking-ts

+

Babylon Bitcoin Staking Protocol

+

TypeScript library

+

+ npm version +

+

+
+ +## 👨🏻‍💻 Installation + +```console +npm i @babylonlabs-io/btc-staking-ts +``` + +## 📝 Commit Format & Automated Releases + +This project uses [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/) +and [**semantic-release**](https://semantic-release.gitbook.io/) to automate +versioning, changelog generation, and npm publishing. +However, release branch will be cut wiht the syntax of `release/vY.X` whenever there is a major version bump. + +### ✅ How It Works + +1. All commits must follow the **Conventional Commits** format. +2. When changes are merged into the `main` branch: + - `semantic-release` analyzes commit messages + - Determines the appropriate semantic version bump (`major`, `minor`, `patch`) + - Updates the `CHANGELOG.md` + - Tags the release in Git + - Publishes the new version to npm (if configured) + +### 🧱 Commit Message Examples + +```console +feat: add support for slashing script +fix: handle invalid staking tx gracefully +docs: update README with commit conventions +refactor!: remove deprecated method and cleanup types +``` + +> **Note:** For breaking changes, add a `!` after the type ( +> e.g. `feat!:` or `refactor!:`) and include a description of the breaking +> change in the commit body. + +### 🚀 Releasing + +Just commit your changes using the proper format and merge to `main`. +The CI pipeline will handle versioning and releasing automatically — no manual +tagging or version bumps needed. + +## 📢 Usage Guide + +Details on the usage of the library can be found +on the [usage guide](./docs/usage.md). diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts new file mode 100644 index 0000000000..e4f8848e27 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts @@ -0,0 +1 @@ +export declare const BTC_DUST_SAT = 546; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js new file mode 100644 index 0000000000..56a8728e7b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BTC_DUST_SAT = void 0; +exports.BTC_DUST_SAT = 546; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts new file mode 100644 index 0000000000..05adbb74e3 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts @@ -0,0 +1,10 @@ +export declare const DEFAULT_INPUT_SIZE = 180; +export declare const P2WPKH_INPUT_SIZE = 68; +export declare const P2TR_INPUT_SIZE = 58; +export declare const TX_BUFFER_SIZE_OVERHEAD = 11; +export declare const LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30; +export declare const MAX_NON_LEGACY_OUTPUT_SIZE = 43; +export declare const WITHDRAW_TX_BUFFER_SIZE = 17; +export declare const WALLET_RELAY_FEE_RATE_THRESHOLD = 2; +export declare const OP_RETURN_OUTPUT_VALUE_SIZE = 8; +export declare const OP_RETURN_VALUE_SERIALIZE_SIZE = 1; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js new file mode 100644 index 0000000000..de3f788be5 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OP_RETURN_VALUE_SERIALIZE_SIZE = exports.OP_RETURN_OUTPUT_VALUE_SIZE = exports.WALLET_RELAY_FEE_RATE_THRESHOLD = exports.WITHDRAW_TX_BUFFER_SIZE = exports.MAX_NON_LEGACY_OUTPUT_SIZE = exports.LOW_RATE_ESTIMATION_ACCURACY_BUFFER = exports.TX_BUFFER_SIZE_OVERHEAD = exports.P2TR_INPUT_SIZE = exports.P2WPKH_INPUT_SIZE = exports.DEFAULT_INPUT_SIZE = void 0; +// Estimated size of a non-SegWit input in bytes +exports.DEFAULT_INPUT_SIZE = 180; +// Estimated size of a P2WPKH input in bytes +exports.P2WPKH_INPUT_SIZE = 68; +// Estimated size of a P2TR input in bytes +exports.P2TR_INPUT_SIZE = 58; +// Estimated size of a transaction buffer in bytes +exports.TX_BUFFER_SIZE_OVERHEAD = 11; +// Buffer for estimation accuracy when fee rate <= 2 sat/byte +exports.LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30; +// Size of a Taproot output, the largest non-legacy output type +exports.MAX_NON_LEGACY_OUTPUT_SIZE = 43; +// Buffer size for withdraw transaction fee calculation +exports.WITHDRAW_TX_BUFFER_SIZE = 17; +// Threshold for wallet relay fee rate. Different buffer fees are used based on this threshold +exports.WALLET_RELAY_FEE_RATE_THRESHOLD = 2; +// Estimated size of the OP_RETURN output value in bytes +exports.OP_RETURN_OUTPUT_VALUE_SIZE = 8; +// Because our OP_RETURN data will always be less than 80 bytes, which is less than 0xfd (253), +// the value serialization size will always be 1 byte. +exports.OP_RETURN_VALUE_SERIALIZE_SIZE = 1; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts new file mode 100644 index 0000000000..e5a7bf8bab --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts @@ -0,0 +1 @@ +export declare const internalPubkey: Buffer; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js new file mode 100644 index 0000000000..1205fa77b1 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.internalPubkey = void 0; +// internalPubkey denotes an unspendable internal public key to be used for the taproot output +const key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; +exports.internalPubkey = Buffer.from(key, "hex").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts new file mode 100644 index 0000000000..c292c88b5a --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts @@ -0,0 +1 @@ +export declare const NO_COORD_PK_BYTE_LENGTH = 32; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js new file mode 100644 index 0000000000..904a4a15b7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js @@ -0,0 +1,5 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NO_COORD_PK_BYTE_LENGTH = void 0; +// NO_COORD_PK_BYTE_LENGTH is the length of a BTC public key without the coordinate in bytes. +exports.NO_COORD_PK_BYTE_LENGTH = 32; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts new file mode 100644 index 0000000000..c1fc587929 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts @@ -0,0 +1,3 @@ +export declare const RBF_SEQUENCE = 4294967293; +export declare const NON_RBF_SEQUENCE = 4294967295; +export declare const TRANSACTION_VERSION = 2; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js new file mode 100644 index 0000000000..64fbdc47e0 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.TRANSACTION_VERSION = exports.NON_RBF_SEQUENCE = exports.RBF_SEQUENCE = void 0; +// This sequence enables both the locktime field and also replace-by-fee +exports.RBF_SEQUENCE = 0xfffffffd; +// This sequence means the transaction is not replaceable +exports.NON_RBF_SEQUENCE = 0xffffffff; +// The Transaction version number used across the library(to be set in the psbt) +exports.TRANSACTION_VERSION = 2; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts new file mode 100644 index 0000000000..c4e0b5a6b8 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts @@ -0,0 +1,3 @@ +export declare const BABYLON_REGISTRY_TYPE_URLS: { + MsgCreateBTCDelegation: string; +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js new file mode 100644 index 0000000000..d405d58347 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BABYLON_REGISTRY_TYPE_URLS = void 0; +exports.BABYLON_REGISTRY_TYPE_URLS = { + MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts new file mode 100644 index 0000000000..a35d33640d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts @@ -0,0 +1 @@ +export declare const REDEEM_VERSION = 192; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js new file mode 100644 index 0000000000..1a2bb2937e --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.REDEEM_VERSION = void 0; +exports.REDEEM_VERSION = 192; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts new file mode 100644 index 0000000000..3d25a9226e --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts @@ -0,0 +1 @@ +export declare const MIN_UNBONDING_OUTPUT_VALUE = 1000; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js new file mode 100644 index 0000000000..ca71d6622e --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MIN_UNBONDING_OUTPUT_VALUE = void 0; +// minimum unbonding output value to avoid the unbonding output value being +// less than Bitcoin dust +exports.MIN_UNBONDING_OUTPUT_VALUE = 1000; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts new file mode 100644 index 0000000000..2ec9884c74 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts @@ -0,0 +1,13 @@ +export declare enum StakingErrorCode { + UNKNOWN_ERROR = "UNKNOWN_ERROR", + INVALID_INPUT = "INVALID_INPUT", + INVALID_OUTPUT = "INVALID_OUTPUT", + SCRIPT_FAILURE = "SCRIPT_FAILURE", + BUILD_TRANSACTION_FAILURE = "BUILD_TRANSACTION_FAILURE", + INVALID_PARAMS = "INVALID_PARAMS" +} +export declare class StakingError extends Error { + code: StakingErrorCode; + constructor(code: StakingErrorCode, message?: string); + static fromUnknown(error: unknown, code: StakingErrorCode, fallbackMsg?: string): StakingError; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js new file mode 100644 index 0000000000..3c571afd78 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StakingError = exports.StakingErrorCode = void 0; +var StakingErrorCode; +(function (StakingErrorCode) { + StakingErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; + StakingErrorCode["INVALID_INPUT"] = "INVALID_INPUT"; + StakingErrorCode["INVALID_OUTPUT"] = "INVALID_OUTPUT"; + StakingErrorCode["SCRIPT_FAILURE"] = "SCRIPT_FAILURE"; + StakingErrorCode["BUILD_TRANSACTION_FAILURE"] = "BUILD_TRANSACTION_FAILURE"; + StakingErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS"; +})(StakingErrorCode || (exports.StakingErrorCode = StakingErrorCode = {})); +class StakingError extends Error { + constructor(code, message) { + super(message); + this.code = code; + } + // Static method to safely handle unknown errors + static fromUnknown(error, code, fallbackMsg) { + if (error instanceof StakingError) { + return error; + } + if (error instanceof Error) { + return new StakingError(code, error.message); + } + return new StakingError(code, fallbackMsg); + } +} +exports.StakingError = StakingError; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts new file mode 100644 index 0000000000..026c91c5e6 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts @@ -0,0 +1,13 @@ +export { StakingScriptData, Staking } from './staking'; +export type { StakingScripts } from './staking'; +export { ObservableStaking, ObservableStakingScriptData } from './staking/observable'; +export * from './staking/transactions'; +export * from './types'; +export * from './utils/btc'; +export * from './utils/babylon'; +export * from './utils/staking'; +export * from './utils/utxo/findInputUTXO'; +export * from './utils/utxo/getPsbtInputFields'; +export * from './utils/utxo/getScriptType'; +export { getBabylonParamByBtcHeight, getBabylonParamByVersion } from './utils/staking/param'; +export * from './staking/manager'; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/index.js new file mode 100644 index 0000000000..02db75a2b8 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/index.js @@ -0,0 +1,35 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getBabylonParamByVersion = exports.getBabylonParamByBtcHeight = exports.ObservableStakingScriptData = exports.ObservableStaking = exports.Staking = exports.StakingScriptData = void 0; +var staking_1 = require("./staking"); +Object.defineProperty(exports, "StakingScriptData", { enumerable: true, get: function () { return staking_1.StakingScriptData; } }); +Object.defineProperty(exports, "Staking", { enumerable: true, get: function () { return staking_1.Staking; } }); +var observable_1 = require("./staking/observable"); +Object.defineProperty(exports, "ObservableStaking", { enumerable: true, get: function () { return observable_1.ObservableStaking; } }); +Object.defineProperty(exports, "ObservableStakingScriptData", { enumerable: true, get: function () { return observable_1.ObservableStakingScriptData; } }); +__exportStar(require("./staking/transactions"), exports); +__exportStar(require("./types"), exports); +__exportStar(require("./utils/btc"), exports); +__exportStar(require("./utils/babylon"), exports); +__exportStar(require("./utils/staking"), exports); +__exportStar(require("./utils/utxo/findInputUTXO"), exports); +__exportStar(require("./utils/utxo/getPsbtInputFields"), exports); +__exportStar(require("./utils/utxo/getScriptType"), exports); +var param_1 = require("./utils/staking/param"); +Object.defineProperty(exports, "getBabylonParamByBtcHeight", { enumerable: true, get: function () { return param_1.getBabylonParamByBtcHeight; } }); +Object.defineProperty(exports, "getBabylonParamByVersion", { enumerable: true, get: function () { return param_1.getBabylonParamByVersion; } }); +__exportStar(require("./staking/manager"), exports); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts new file mode 100644 index 0000000000..c7e95fa536 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts @@ -0,0 +1,117 @@ +import { networks, Psbt, Transaction } from "bitcoinjs-lib"; +import { StakingParams } from "../types/params"; +import { UTXO } from "../types/UTXO"; +import { StakingScripts } from "./stakingScript"; +import { PsbtResult, TransactionResult } from "../types/transaction"; +export * from "./stakingScript"; +export interface StakerInfo { + address: string; + publicKeyNoCoordHex: string; +} +export declare class Staking { + network: networks.Network; + stakerInfo: StakerInfo; + params: StakingParams; + finalityProviderPkNoCoordHex: string; + stakingTimelock: number; + constructor(network: networks.Network, stakerInfo: StakerInfo, params: StakingParams, finalityProviderPkNoCoordHex: string, stakingTimelock: number); + /** + * buildScripts builds the staking scripts for the staking transaction. + * Note: different staking types may have different scripts. + * e.g the observable staking script has a data embed script. + * + * @returns {StakingScripts} - The staking scripts. + */ + buildScripts(): StakingScripts; + /** + * Create a staking transaction for staking. + * + * @param {number} stakingAmountSat - The amount to stake in satoshis. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {TransactionResult} - An object containing the unsigned + * transaction, and fee + * @throws {StakingError} - If the transaction cannot be built + */ + createStakingTransaction(stakingAmountSat: number, inputUTXOs: UTXO[], feeRate: number): TransactionResult; + /** + * Create a staking psbt based on the existing staking transaction. + * + * @param {Transaction} stakingTx - The staking transaction. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. The UTXOs that were used to create the staking transaction should + * be included in this array. + * @returns {Psbt} - The psbt. + */ + toStakingPsbt(stakingTx: Transaction, inputUTXOs: UTXO[]): Psbt; + /** + * Create an unbonding transaction for staking. + * + * @param {Transaction} stakingTx - The staking transaction to unbond. + * @returns {TransactionResult} - An object containing the unsigned + * transaction, and fee + * @throws {StakingError} - If the transaction cannot be built + */ + createUnbondingTransaction(stakingTx: Transaction): TransactionResult; + /** + * Create an unbonding psbt based on the existing unbonding transaction and + * staking transaction. + * + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {Transaction} stakingTx - The staking transaction. + * + * @returns {Psbt} - The psbt. + */ + toUnbondingPsbt(unbondingTx: Transaction, stakingTx: Transaction): Psbt; + /** + * Creates a withdrawal transaction that spends from an unbonding or slashing + * transaction. The timelock on the input transaction must have expired before + * this withdrawal can be valid. + * + * @param {Transaction} earlyUnbondedTx - The unbonding or slashing + * transaction to withdraw from + * @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal + * transaction + * @returns {PsbtResult} - Contains the unsigned PSBT and fee amount + * @throws {StakingError} - If the input transaction is invalid or withdrawal + * transaction cannot be built + */ + createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx: Transaction, feeRate: number): PsbtResult; + /** + * Create a withdrawal psbt that spends a naturally expired staking + * transaction. + * + * @param {Transaction} stakingTx - The staking transaction to withdraw from. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createWithdrawStakingExpiredPsbt(stakingTx: Transaction, feeRate: number): PsbtResult; + /** + * Create a slashing psbt spending from the staking output. + * + * @param {Transaction} stakingTx - The staking transaction to slash. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createStakingOutputSlashingPsbt(stakingTx: Transaction): PsbtResult; + /** + * Create a slashing psbt for an unbonding output. + * + * @param {Transaction} unbondingTx - The unbonding transaction to slash. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createUnbondingOutputSlashingPsbt(unbondingTx: Transaction): PsbtResult; + /** + * Create a withdraw slashing psbt that spends a slashing transaction from the + * staking output. + * + * @param {Transaction} slashingTx - The slashing transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createWithdrawSlashingPsbt(slashingTx: Transaction, feeRate: number): PsbtResult; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js new file mode 100644 index 0000000000..5a48f3f6b7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js @@ -0,0 +1,274 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Staking = void 0; +const stakingScript_1 = require("./stakingScript"); +const error_1 = require("../error"); +const transactions_1 = require("./transactions"); +const btc_1 = require("../utils/btc"); +const staking_1 = require("../utils/staking"); +const staking_2 = require("../utils/staking"); +const psbt_1 = require("./psbt"); +__exportStar(require("./stakingScript"), exports); +class Staking { + constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) { + // Perform validations + if (!(0, btc_1.isValidBitcoinAddress)(stakerInfo.address, network)) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staker bitcoin address"); + } + if (!(0, btc_1.isValidNoCoordPublicKey)(stakerInfo.publicKeyNoCoordHex)) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staker public key"); + } + if (!(0, btc_1.isValidNoCoordPublicKey)(finalityProviderPkNoCoordHex)) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid finality provider public key"); + } + (0, staking_1.validateParams)(params); + (0, staking_1.validateStakingTimelock)(stakingTimelock, params); + this.network = network; + this.stakerInfo = stakerInfo; + this.params = params; + this.finalityProviderPkNoCoordHex = finalityProviderPkNoCoordHex; + this.stakingTimelock = stakingTimelock; + } + /** + * buildScripts builds the staking scripts for the staking transaction. + * Note: different staking types may have different scripts. + * e.g the observable staking script has a data embed script. + * + * @returns {StakingScripts} - The staking scripts. + */ + buildScripts() { + const { covenantQuorum, covenantNoCoordPks, unbondingTime } = this.params; + // Create staking script data + let stakingScriptData; + try { + stakingScriptData = new stakingScript_1.StakingScriptData(Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"), [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")], (0, staking_2.toBuffers)(covenantNoCoordPks), covenantQuorum, this.stakingTimelock, unbondingTime); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking script data"); + } + // Build scripts + let scripts; + try { + scripts = stakingScriptData.buildScripts(); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking scripts"); + } + return scripts; + } + /** + * Create a staking transaction for staking. + * + * @param {number} stakingAmountSat - The amount to stake in satoshis. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {TransactionResult} - An object containing the unsigned + * transaction, and fee + * @throws {StakingError} - If the transaction cannot be built + */ + createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) { + (0, staking_1.validateStakingTxInputData)(stakingAmountSat, this.stakingTimelock, this.params, inputUTXOs, feeRate); + const scripts = this.buildScripts(); + try { + const { transaction, fee } = (0, transactions_1.stakingTransaction)(scripts, stakingAmountSat, this.stakerInfo.address, inputUTXOs, this.network, feeRate); + return { + transaction, + fee, + }; + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned staking transaction"); + } + } + ; + /** + * Create a staking psbt based on the existing staking transaction. + * + * @param {Transaction} stakingTx - The staking transaction. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. The UTXOs that were used to create the staking transaction should + * be included in this array. + * @returns {Psbt} - The psbt. + */ + toStakingPsbt(stakingTx, inputUTXOs) { + // Check the staking output index can be found + const scripts = this.buildScripts(); + const stakingOutputInfo = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); + (0, staking_1.findMatchingTxOutputIndex)(stakingTx, stakingOutputInfo.outputAddress, this.network); + return (0, psbt_1.stakingPsbt)(stakingTx, this.network, inputUTXOs, (0, btc_1.isTaproot)(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : undefined); + } + /** + * Create an unbonding transaction for staking. + * + * @param {Transaction} stakingTx - The staking transaction to unbond. + * @returns {TransactionResult} - An object containing the unsigned + * transaction, and fee + * @throws {StakingError} - If the transaction cannot be built + */ + createUnbondingTransaction(stakingTx) { + // Build scripts + const scripts = this.buildScripts(); + const { outputAddress } = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); + // Reconstruct the stakingOutputIndex + const stakingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(stakingTx, outputAddress, this.network); + // Create the unbonding transaction + try { + const { transaction } = (0, transactions_1.unbondingTransaction)(scripts, stakingTx, this.params.unbondingFeeSat, this.network, stakingOutputIndex); + return { + transaction, + fee: this.params.unbondingFeeSat, + }; + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the unbonding transaction"); + } + } + /** + * Create an unbonding psbt based on the existing unbonding transaction and + * staking transaction. + * + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {Transaction} stakingTx - The staking transaction. + * + * @returns {Psbt} - The psbt. + */ + toUnbondingPsbt(unbondingTx, stakingTx) { + return (0, psbt_1.unbondingPsbt)(this.buildScripts(), unbondingTx, stakingTx, this.network); + } + /** + * Creates a withdrawal transaction that spends from an unbonding or slashing + * transaction. The timelock on the input transaction must have expired before + * this withdrawal can be valid. + * + * @param {Transaction} earlyUnbondedTx - The unbonding or slashing + * transaction to withdraw from + * @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal + * transaction + * @returns {PsbtResult} - Contains the unsigned PSBT and fee amount + * @throws {StakingError} - If the input transaction is invalid or withdrawal + * transaction cannot be built + */ + createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx, feeRate) { + // Build scripts + const scripts = this.buildScripts(); + // Create the withdraw early unbonded transaction + try { + return (0, transactions_1.withdrawEarlyUnbondedTransaction)(scripts, earlyUnbondedTx, this.stakerInfo.address, this.network, feeRate); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned withdraw early unbonded transaction"); + } + } + /** + * Create a withdrawal psbt that spends a naturally expired staking + * transaction. + * + * @param {Transaction} stakingTx - The staking transaction to withdraw from. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createWithdrawStakingExpiredPsbt(stakingTx, feeRate) { + // Build scripts + const scripts = this.buildScripts(); + const { outputAddress } = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); + // Reconstruct the stakingOutputIndex + const stakingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(stakingTx, outputAddress, this.network); + // Create the timelock unbonded transaction + try { + return (0, transactions_1.withdrawTimelockUnbondedTransaction)(scripts, stakingTx, this.stakerInfo.address, this.network, feeRate, stakingOutputIndex); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned timelock unbonded transaction"); + } + } + /** + * Create a slashing psbt spending from the staking output. + * + * @param {Transaction} stakingTx - The staking transaction to slash. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createStakingOutputSlashingPsbt(stakingTx) { + if (!this.params.slashing) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing parameters are missing"); + } + // Build scripts + const scripts = this.buildScripts(); + // create the slash timelock unbonded transaction + try { + const { psbt } = (0, transactions_1.slashTimelockUnbondedTransaction)(scripts, stakingTx, this.params.slashing.slashingPkScriptHex, this.params.slashing.slashingRate, this.params.slashing.minSlashingTxFeeSat, this.network); + return { + psbt, + fee: this.params.slashing.minSlashingTxFeeSat, + }; + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the slash timelock unbonded transaction"); + } + } + /** + * Create a slashing psbt for an unbonding output. + * + * @param {Transaction} unbondingTx - The unbonding transaction to slash. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createUnbondingOutputSlashingPsbt(unbondingTx) { + if (!this.params.slashing) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing parameters are missing"); + } + // Build scripts + const scripts = this.buildScripts(); + // create the slash timelock unbonded transaction + try { + const { psbt } = (0, transactions_1.slashEarlyUnbondedTransaction)(scripts, unbondingTx, this.params.slashing.slashingPkScriptHex, this.params.slashing.slashingRate, this.params.slashing.minSlashingTxFeeSat, this.network); + return { + psbt, + fee: this.params.slashing.minSlashingTxFeeSat, + }; + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the slash early unbonded transaction"); + } + } + /** + * Create a withdraw slashing psbt that spends a slashing transaction from the + * staking output. + * + * @param {Transaction} slashingTx - The slashing transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} - An object containing the unsigned psbt and fee + * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built + */ + createWithdrawSlashingPsbt(slashingTx, feeRate) { + // Build scripts + const scripts = this.buildScripts(); + const slashingOutputInfo = (0, staking_1.deriveSlashingOutput)(scripts, this.network); + // Reconstruct and validate the slashingOutputIndex + const slashingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(slashingTx, slashingOutputInfo.outputAddress, this.network); + // Create the withdraw slashed transaction + try { + return (0, transactions_1.withdrawSlashingTransaction)(scripts, slashingTx, this.stakerInfo.address, this.network, feeRate, slashingOutputIndex); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build withdraw slashing transaction"); + } + } +} +exports.Staking = Staking; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts new file mode 100644 index 0000000000..4f9b38ee28 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts @@ -0,0 +1,246 @@ +import { networks, Transaction } from "bitcoinjs-lib"; +import { StakingParams, VersionedStakingParams } from "../types/params"; +import { TransactionResult, UTXO } from "../types"; +import { StakerInfo, Staking } from "."; +import { btcstaking, btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { ProofOfPossessionBTC } from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; +export interface BtcProvider { + signPsbt(signingStep: SigningStep, psbtHex: string): Promise; + signMessage: (signingStep: SigningStep, message: string, type: "ecdsa" | "bip322-simple") => Promise; +} +export interface BabylonProvider { + /** + * Signs a Babylon chain transaction using the provided signing step. + * This is primarily used for signing MsgCreateBTCDelegation transactions + * which register the BTC delegation on the Babylon Genesis chain. + * + * @param {SigningStep} signingStep - The current signing step context + * @param {object} msg - The Cosmos SDK transaction message to sign + * @param {string} msg.typeUrl - The Protobuf type URL identifying the message type + * @param {T} msg.value - The transaction message data matching the typeUrl + * @returns {Promise} The signed transaction bytes + */ + signTransaction: (signingStep: SigningStep, msg: { + typeUrl: string; + value: T; + }) => Promise; +} +export declare enum SigningStep { + STAKING_SLASHING = "staking-slashing", + UNBONDING_SLASHING = "unbonding-slashing", + PROOF_OF_POSSESSION = "proof-of-possession", + CREATE_BTC_DELEGATION_MSG = "create-btc-delegation-msg", + STAKING = "staking", + UNBONDING = "unbonding", + WITHDRAW_STAKING_EXPIRED = "withdraw-staking-expired", + WITHDRAW_EARLY_UNBONDED = "withdraw-early-unbonded", + WITHDRAW_SLASHING = "withdraw-slashing" +} +interface StakingInputs { + finalityProviderPkNoCoordHex: string; + stakingAmountSat: number; + stakingTimelock: number; +} +interface InclusionProof { + pos: number; + merkle: string[]; + blockHashHex: string; +} +export declare class BabylonBtcStakingManager { + protected stakingParams: VersionedStakingParams[]; + protected btcProvider: BtcProvider; + protected network: networks.Network; + protected babylonProvider: BabylonProvider; + constructor(network: networks.Network, stakingParams: VersionedStakingParams[], btcProvider: BtcProvider, babylonProvider: BabylonProvider); + /** + * Creates a signed Pre-Staking Registration transaction that is ready to be + * sent to the Babylon chain. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param babylonBtcTipHeight - The Babylon BTC tip height. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @param babylonAddress - The Babylon bech32 encoded address of the staker. + * @returns The signed babylon pre-staking registration transaction in base64 + * format. + */ + preStakeRegistrationBabylonTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, babylonBtcTipHeight: number, inputUTXOs: UTXO[], feeRate: number, babylonAddress: string): Promise<{ + signedBabylonTx: Uint8Array; + stakingTx: Transaction; + }>; + /** + * Creates a signed post-staking registration transaction that is ready to be + * sent to the Babylon chain. This is used when a staking transaction is + * already created and included in a BTC block and we want to register it on + * the Babylon chain. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingTx - The staking transaction. + * @param stakingTxHeight - The BTC height in which the staking transaction + * is included. + * @param stakingInput - The staking inputs. + * @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction + * inclusion in a Bitcoin block that is k-deep. + * @param babylonAddress - The Babylon bech32 encoded address of the staker. + * @returns The signed babylon transaction in base64 format. + */ + postStakeRegistrationBabylonTransaction(stakerBtcInfo: StakerInfo, stakingTx: Transaction, stakingTxHeight: number, stakingInput: StakingInputs, inclusionProof: InclusionProof, babylonAddress: string): Promise<{ + signedBabylonTx: Uint8Array; + }>; + /** + * Estimates the BTC fee required for staking. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon + * chain. + * @param stakingInput - The staking inputs. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The estimated BTC fee in satoshis. + */ + estimateBtcStakingFee(stakerBtcInfo: StakerInfo, babylonBtcTipHeight: number, stakingInput: StakingInputs, inputUTXOs: UTXO[], feeRate: number): number; + /** + * Creates a signed staking transaction that is ready to be sent to the BTC + * network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param unsignedStakingTx - The unsigned staking transaction. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @returns The signed staking transaction. + */ + createSignedBtcStakingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, unsignedStakingTx: Transaction, inputUTXOs: UTXO[], stakingParamsVersion: number): Promise; + /** + * Creates a partial signed unbonding transaction that is only signed by the + * staker. In order to complete the unbonding transaction, the covenant + * unbonding signatures need to be added to the transaction before sending it + * to the BTC network. + * NOTE: This method should only be used for Babylon phase-1 unbonding. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @returns The partial signed unbonding transaction and its fee. + */ + createPartialSignedBtcUnbondingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction): Promise; + /** + * Creates a signed unbonding transaction that is ready to be sent to the BTC + * network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @param unsignedUnbondingTx - The unsigned unbonding transaction. + * @param covenantUnbondingSignatures - The covenant unbonding signatures. + * It can be retrieved from the Babylon chain or API. + * @returns The signed unbonding transaction and its fee. + */ + createSignedBtcUnbondingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction, unsignedUnbondingTx: Transaction, covenantUnbondingSignatures: { + btcPkHex: string; + sigHex: string; + }[]): Promise; + /** + * Creates a signed withdrawal transaction on the unbodning output expiry path + * that is ready to be sent to the BTC network. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param earlyUnbondingTx - The early unbonding transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, earlyUnbondingTx: Transaction, feeRate: number): Promise; + /** + * Creates a signed withdrawal transaction on the staking output expiry path + * that is ready to be sent to the BTC network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction, feeRate: number): Promise; + /** + * Creates a signed withdrawal transaction for the expired slashing output that + * is ready to be sent to the BTC network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param slashingTx - The slashing transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, slashingTx: Transaction, feeRate: number): Promise; + /** + * Creates a proof of possession for the staker based on ECDSA signature. + * @param bech32Address - The staker's bech32 address on the babylon chain + * @param stakerBtcAddress - The staker's BTC address. + * @returns The proof of possession. + */ + createProofOfPossession(bech32Address: string, stakerBtcAddress: string): Promise; + /** + * Creates the unbonding, slashing, and unbonding slashing transactions and + * PSBTs. + * @param stakingInstance - The staking instance. + * @param stakingTx - The staking transaction. + * @returns The unbonding, slashing, and unbonding slashing transactions and + * PSBTs. + */ + private createDelegationTransactionsAndPsbts; + /** + * Creates a protobuf message for the BTC delegation. + * @param stakingInstance - The staking instance. + * @param stakingInput - The staking inputs. + * @param stakingTx - The staking transaction. + * @param bech32Address - The staker's babylon chain bech32 address + * @param stakerBtcInfo - The staker's BTC information such as address and + * public key + * @param params - The staking parameters. + * @param inclusionProof - The inclusion proof of the staking transaction. + * @returns The protobuf message. + */ + createBtcDelegationMsg(stakingInstance: Staking, stakingInput: StakingInputs, stakingTx: Transaction, bech32Address: string, stakerBtcInfo: StakerInfo, params: StakingParams, inclusionProof?: btcstaking.InclusionProof): Promise<{ + typeUrl: string; + value: btcstakingtx.MsgCreateBTCDelegation; + }>; + /** + * Gets the inclusion proof for the staking transaction. + * See the type `InclusionProof` for more information + * @param inclusionProof - The inclusion proof. + * @returns The inclusion proof. + */ + private getInclusionProof; +} +/** + * Get the staker signature from the unbonding transaction + * This is used mostly for unbonding transactions from phase-1(Observable) + * @param unbondingTx - The unbonding transaction + * @returns The staker signature + */ +export declare const getUnbondingTxStakerSignature: (unbondingTx: Transaction) => string; +export {}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js new file mode 100644 index 0000000000..4ae5ff10de --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js @@ -0,0 +1,524 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getUnbondingTxStakerSignature = exports.BabylonBtcStakingManager = exports.SigningStep = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const _1 = require("."); +const babylon_proto_ts_1 = require("@babylonlabs-io/babylon-proto-ts"); +const pop_1 = require("@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"); +const registry_1 = require("../constants/registry"); +const transactions_1 = require("./transactions"); +const param_1 = require("../utils/staking/param"); +const utils_1 = require("../utils"); +const staking_1 = require("../utils/staking"); +const staking_2 = require("../utils/staking"); +const babylon_1 = require("../utils/babylon"); +const error_1 = require("../error"); +const error_2 = require("../error"); +const btc_1 = require("../utils/btc"); +// Event types for the Signing event +var SigningStep; +(function (SigningStep) { + SigningStep["STAKING_SLASHING"] = "staking-slashing"; + SigningStep["UNBONDING_SLASHING"] = "unbonding-slashing"; + SigningStep["PROOF_OF_POSSESSION"] = "proof-of-possession"; + SigningStep["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg"; + SigningStep["STAKING"] = "staking"; + SigningStep["UNBONDING"] = "unbonding"; + SigningStep["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired"; + SigningStep["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded"; + SigningStep["WITHDRAW_SLASHING"] = "withdraw-slashing"; +})(SigningStep || (exports.SigningStep = SigningStep = {})); +class BabylonBtcStakingManager { + constructor(network, stakingParams, btcProvider, babylonProvider) { + this.network = network; + this.btcProvider = btcProvider; + this.babylonProvider = babylonProvider; + if (stakingParams.length === 0) { + throw new Error("No staking parameters provided"); + } + this.stakingParams = stakingParams; + } + /** + * Creates a signed Pre-Staking Registration transaction that is ready to be + * sent to the Babylon chain. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param babylonBtcTipHeight - The Babylon BTC tip height. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @param babylonAddress - The Babylon bech32 encoded address of the staker. + * @returns The signed babylon pre-staking registration transaction in base64 + * format. + */ + preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) { + return __awaiter(this, void 0, void 0, function* () { + if (babylonBtcTipHeight === 0) { + throw new Error("Babylon BTC tip height cannot be 0"); + } + if (inputUTXOs.length === 0) { + throw new Error("No input UTXOs provided"); + } + if (!(0, babylon_1.isValidBabylonAddress)(babylonAddress)) { + throw new Error("Invalid Babylon address"); + } + // Get the Babylon params based on the BTC tip height from Babylon chain + const params = (0, param_1.getBabylonParamByBtcHeight)(babylonBtcTipHeight, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + // Create unsigned staking transaction + const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); + // Create delegation message without including inclusion proof + const msg = yield this.createBtcDelegationMsg(staking, stakingInput, transaction, babylonAddress, stakerBtcInfo, params); + return { + signedBabylonTx: yield this.babylonProvider.signTransaction(SigningStep.CREATE_BTC_DELEGATION_MSG, msg), + stakingTx: transaction, + }; + }); + } + /** + * Creates a signed post-staking registration transaction that is ready to be + * sent to the Babylon chain. This is used when a staking transaction is + * already created and included in a BTC block and we want to register it on + * the Babylon chain. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingTx - The staking transaction. + * @param stakingTxHeight - The BTC height in which the staking transaction + * is included. + * @param stakingInput - The staking inputs. + * @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction + * inclusion in a Bitcoin block that is k-deep. + * @param babylonAddress - The Babylon bech32 encoded address of the staker. + * @returns The signed babylon transaction in base64 format. + */ + postStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingTx, stakingTxHeight, stakingInput, inclusionProof, babylonAddress) { + return __awaiter(this, void 0, void 0, function* () { + // Get the Babylon params at the time of the staking transaction + const params = (0, param_1.getBabylonParamByBtcHeight)(stakingTxHeight, this.stakingParams); + if (!(0, babylon_1.isValidBabylonAddress)(babylonAddress)) { + throw new Error("Invalid Babylon address"); + } + const stakingInstance = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + // Validate if the stakingTx is valid based on the retrieved Babylon param + const scripts = stakingInstance.buildScripts(); + const stakingOutputInfo = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); + // Error will be thrown if the expected staking output address is not found + // in the stakingTx + (0, staking_2.findMatchingTxOutputIndex)(stakingTx, stakingOutputInfo.outputAddress, this.network); + // Create delegation message + const delegationMsg = yield this.createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, babylonAddress, stakerBtcInfo, params, this.getInclusionProof(inclusionProof)); + return { + signedBabylonTx: yield this.babylonProvider.signTransaction(SigningStep.CREATE_BTC_DELEGATION_MSG, delegationMsg), + }; + }); + } + /** + * Estimates the BTC fee required for staking. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon + * chain. + * @param stakingInput - The staking inputs. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The estimated BTC fee in satoshis. + */ + estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) { + if (babylonBtcTipHeight === 0) { + throw new Error("Babylon BTC tip height cannot be 0"); + } + // Get the param based on the tip height + const params = (0, param_1.getBabylonParamByBtcHeight)(babylonBtcTipHeight, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const { fee: stakingFee } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); + return stakingFee; + } + /** + * Creates a signed staking transaction that is ready to be sent to the BTC + * network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param unsignedStakingTx - The unsigned staking transaction. + * @param inputUTXOs - The UTXOs that will be used to pay for the staking + * transaction. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @returns The signed staking transaction. + */ + createSignedBtcStakingTransaction(stakerBtcInfo, stakingInput, unsignedStakingTx, inputUTXOs, stakingParamsVersion) { + return __awaiter(this, void 0, void 0, function* () { + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + if (inputUTXOs.length === 0) { + throw new Error("No input UTXOs provided"); + } + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const stakingPsbt = staking.toStakingPsbt(unsignedStakingTx, inputUTXOs); + const signedStakingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.STAKING, stakingPsbt.toHex()); + return bitcoinjs_lib_1.Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); + }); + } + /** + * Creates a partial signed unbonding transaction that is only signed by the + * staker. In order to complete the unbonding transaction, the covenant + * unbonding signatures need to be added to the transaction before sending it + * to the BTC network. + * NOTE: This method should only be used for Babylon phase-1 unbonding. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @returns The partial signed unbonding transaction and its fee. + */ + createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) { + return __awaiter(this, void 0, void 0, function* () { + // Get the staking params at the time of the staking transaction + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const { transaction: unbondingTx, fee, } = staking.createUnbondingTransaction(stakingTx); + const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx); + const signedUnbondingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.UNBONDING, psbt.toHex()); + const signedUnbondingTx = bitcoinjs_lib_1.Psbt.fromHex(signedUnbondingPsbtHex).extractTransaction(); + return { + transaction: signedUnbondingTx, + fee, + }; + }); + } + /** + * Creates a signed unbonding transaction that is ready to be sent to the BTC + * network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @param unsignedUnbondingTx - The unsigned unbonding transaction. + * @param covenantUnbondingSignatures - The covenant unbonding signatures. + * It can be retrieved from the Babylon chain or API. + * @returns The signed unbonding transaction and its fee. + */ + createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) { + return __awaiter(this, void 0, void 0, function* () { + // Get the staking params at the time of the staking transaction + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + const { transaction: signedUnbondingTx, fee, } = yield this.createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx); + // Check the computed txid of the signed unbonding transaction is the same as + // the txid of the unsigned unbonding transaction + if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) { + throw new Error("Unbonding transaction hash does not match the computed hash"); + } + // Add covenant unbonding signatures + // Convert the params of covenants to buffer + const covenantBuffers = params.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex")); + const witness = (0, transactions_1.createCovenantWitness)( + // Since unbonding transactions always have a single input and output, + // we expect exactly one signature in TaprootScriptSpendSig when the + // signing is successful + signedUnbondingTx.ins[0].witness, covenantBuffers, covenantUnbondingSignatures, params.covenantQuorum); + // Overwrite the witness to include the covenant unbonding signatures + signedUnbondingTx.ins[0].witness = witness; + return { + transaction: signedUnbondingTx, + fee, + }; + }); + } + /** + * Creates a signed withdrawal transaction on the unbodning output expiry path + * that is ready to be sent to the BTC network. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param earlyUnbondingTx - The early unbonding transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) { + return __awaiter(this, void 0, void 0, function* () { + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const { psbt: unbondingPsbt, fee } = staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate); + const signedWithdrawalPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_EARLY_UNBONDED, unbondingPsbt.toHex()); + return { + transaction: bitcoinjs_lib_1.Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), + fee, + }; + }); + } + /** + * Creates a signed withdrawal transaction on the staking output expiry path + * that is ready to be sent to the BTC network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param stakingTx - The staking transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) { + return __awaiter(this, void 0, void 0, function* () { + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(stakingTx, feeRate); + const signedWithdrawalPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_STAKING_EXPIRED, psbt.toHex()); + return { + transaction: bitcoinjs_lib_1.Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), + fee, + }; + }); + } + /** + * Creates a signed withdrawal transaction for the expired slashing output that + * is ready to be sent to the BTC network. + * @param stakerBtcInfo - The staker BTC info which includes the BTC address + * and the no-coord public key in hex format. + * @param stakingInput - The staking inputs. + * @param stakingParamsVersion - The params version that was used to create the + * delegation in Babylon chain + * @param slashingTx - The slashing transaction. + * @param feeRate - The fee rate in satoshis per byte. Typical value for the + * fee rate is above 1. If the fee rate is too low, the transaction will not + * be included in a block. + * @returns The signed withdrawal transaction and its fee. + */ + createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) { + return __awaiter(this, void 0, void 0, function* () { + const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); + const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); + const { psbt, fee } = staking.createWithdrawSlashingPsbt(slashingTx, feeRate); + const signedSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_SLASHING, psbt.toHex()); + return { + transaction: bitcoinjs_lib_1.Psbt.fromHex(signedSlashingPsbtHex).extractTransaction(), + fee, + }; + }); + } + /** + * Creates a proof of possession for the staker based on ECDSA signature. + * @param bech32Address - The staker's bech32 address on the babylon chain + * @param stakerBtcAddress - The staker's BTC address. + * @returns The proof of possession. + */ + createProofOfPossession(bech32Address, stakerBtcAddress) { + return __awaiter(this, void 0, void 0, function* () { + let sigType = pop_1.BTCSigType.ECDSA; + // For Taproot or Native SegWit addresses, use the BIP322 signature scheme + // in the proof of possession as it uses the same signature type as the regular + // input UTXO spend. For legacy addresses, use the ECDSA signature scheme. + if ((0, btc_1.isTaproot)(stakerBtcAddress, this.network) + || (0, btc_1.isNativeSegwit)(stakerBtcAddress, this.network)) { + sigType = pop_1.BTCSigType.BIP322; + } + const signedBabylonAddress = yield this.btcProvider.signMessage(SigningStep.PROOF_OF_POSSESSION, bech32Address, sigType === pop_1.BTCSigType.BIP322 ? "bip322-simple" : "ecdsa"); + let btcSig; + if (sigType === pop_1.BTCSigType.BIP322) { + const bip322Sig = pop_1.BIP322Sig.fromPartial({ + address: stakerBtcAddress, + sig: Buffer.from(signedBabylonAddress, "base64"), + }); + // Encode the BIP322 protobuf message to a Uint8Array + btcSig = pop_1.BIP322Sig.encode(bip322Sig).finish(); + } + else { + // Encode the ECDSA signature to a Uint8Array + btcSig = Buffer.from(signedBabylonAddress, "base64"); + } + return { + btcSigType: sigType, + btcSig + }; + }); + } + /** + * Creates the unbonding, slashing, and unbonding slashing transactions and + * PSBTs. + * @param stakingInstance - The staking instance. + * @param stakingTx - The staking transaction. + * @returns The unbonding, slashing, and unbonding slashing transactions and + * PSBTs. + */ + createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) { + return __awaiter(this, void 0, void 0, function* () { + const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction(stakingTx); + // Create slashing transactions and extract signatures + const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingPsbt(stakingTx); + const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx); + return { + unbondingTx, + slashingPsbt, + unbondingSlashingPsbt, + }; + }); + } + /** + * Creates a protobuf message for the BTC delegation. + * @param stakingInstance - The staking instance. + * @param stakingInput - The staking inputs. + * @param stakingTx - The staking transaction. + * @param bech32Address - The staker's babylon chain bech32 address + * @param stakerBtcInfo - The staker's BTC information such as address and + * public key + * @param params - The staking parameters. + * @param inclusionProof - The inclusion proof of the staking transaction. + * @returns The protobuf message. + */ + createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, inclusionProof) { + return __awaiter(this, void 0, void 0, function* () { + const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = yield this.createDelegationTransactionsAndPsbts(stakingInstance, stakingTx); + // Sign the slashing PSBT + const signedSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.STAKING_SLASHING, slashingPsbt.toHex()); + const signedSlashingTx = bitcoinjs_lib_1.Psbt.fromHex(signedSlashingPsbtHex).extractTransaction(); + const slashingSig = extractFirstSchnorrSignatureFromTransaction(signedSlashingTx); + if (!slashingSig) { + throw new Error("No signature found in the staking output slashing PSBT"); + } + // Sign the unbonding slashing PSBT + const signedUnbondingSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.UNBONDING_SLASHING, unbondingSlashingPsbt.toHex()); + const signedUnbondingSlashingTx = bitcoinjs_lib_1.Psbt.fromHex(signedUnbondingSlashingPsbtHex).extractTransaction(); + const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(signedUnbondingSlashingTx); + if (!unbondingSignatures) { + throw new Error("No signature found in the unbonding output slashing PSBT"); + } + // Create proof of possession + const proofOfPossession = yield this.createProofOfPossession(bech32Address, stakerBtcInfo.address); + // Prepare the final protobuf message + const msg = babylon_proto_ts_1.btcstakingtx.MsgCreateBTCDelegation.fromPartial({ + stakerAddr: bech32Address, + pop: proofOfPossession, + btcPk: Uint8Array.from(Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")), + fpBtcPkList: [ + Uint8Array.from(Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")), + ], + stakingTime: stakingInput.stakingTimelock, + stakingValue: stakingInput.stakingAmountSat, + stakingTx: Uint8Array.from(stakingTx.toBuffer()), + slashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")), + delegatorSlashingSig: Uint8Array.from(slashingSig), + unbondingTime: params.unbondingTime, + unbondingTx: Uint8Array.from(unbondingTx.toBuffer()), + unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat, + unbondingSlashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedUnbondingSlashingTx).toHex(), "hex")), + delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), + stakingTxInclusionProof: inclusionProof, + }); + return { + typeUrl: registry_1.BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation, + value: msg, + }; + }); + } + ; + /** + * Gets the inclusion proof for the staking transaction. + * See the type `InclusionProof` for more information + * @param inclusionProof - The inclusion proof. + * @returns The inclusion proof. + */ + getInclusionProof(inclusionProof) { + const { pos, merkle, blockHashHex } = inclusionProof; + const proofHex = deriveMerkleProof(merkle); + const hash = (0, utils_1.reverseBuffer)(Uint8Array.from(Buffer.from(blockHashHex, "hex"))); + const inclusionProofKey = babylon_proto_ts_1.btccheckpoint.TransactionKey.fromPartial({ + index: pos, + hash, + }); + return babylon_proto_ts_1.btcstaking.InclusionProof.fromPartial({ + key: inclusionProofKey, + proof: Uint8Array.from(Buffer.from(proofHex, "hex")), + }); + } + ; +} +exports.BabylonBtcStakingManager = BabylonBtcStakingManager; +/** + * Extracts the first valid Schnorr signature from a signed transaction. + * + * Since we only handle transactions with a single input and request a signature + * for one public key, there can be at most one signature from the Bitcoin node. + * A valid Schnorr signature is exactly 64 bytes in length. + * + * @param singedTransaction - The signed Bitcoin transaction to extract the signature from + * @returns The first valid 64-byte Schnorr signature found in the transaction witness data, + * or undefined if no valid signature exists + */ +const extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => { + // Loop through each input to extract the witness signature + for (const input of singedTransaction.ins) { + if (input.witness && input.witness.length > 0) { + const schnorrSignature = input.witness[0]; + // Check that it's a 64-byte Schnorr signature + if (schnorrSignature.length === 64) { + return schnorrSignature; // Return the first valid signature found + } + } + } + return undefined; +}; +/** + * Strips all signatures from a transaction by clearing both the script and + * witness data. This is due to the fact that we only need the raw unsigned + * transaction structure. The signatures are sent in a separate protobuf field + * when creating the delegation message in the Babylon. + * @param tx - The transaction to strip signatures from + * @returns A copy of the transaction with all signatures removed + */ +const clearTxSignatures = (tx) => { + tx.ins.forEach((input) => { + input.script = Buffer.alloc(0); + input.witness = []; + }); + return tx; +}; +/** + * Derives the merkle proof from the list of hex strings. Note the + * sibling hashes are reversed from hex before concatenation. + * @param merkle - The merkle proof hex strings. + * @returns The merkle proof in hex string format. + */ +const deriveMerkleProof = (merkle) => { + const proofHex = merkle.reduce((acc, m) => { + return acc + Buffer.from(m, "hex").reverse().toString("hex"); + }, ""); + return proofHex; +}; +/** + * Get the staker signature from the unbonding transaction + * This is used mostly for unbonding transactions from phase-1(Observable) + * @param unbondingTx - The unbonding transaction + * @returns The staker signature + */ +const getUnbondingTxStakerSignature = (unbondingTx) => { + try { + // There is only one input and one output in the unbonding transaction + return unbondingTx.ins[0].witness[0].toString("hex"); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_2.StakingErrorCode.INVALID_INPUT, "Failed to get staker signature"); + } +}; +exports.getUnbondingTxStakerSignature = getUnbondingTxStakerSignature; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts new file mode 100644 index 0000000000..9a26613415 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts @@ -0,0 +1,53 @@ +import { ObservableVersionedStakingParams } from "../../types/params"; +import { UTXO } from "../../types/UTXO"; +import { TransactionResult } from "../../types/transaction"; +import { ObservableStakingScripts } from "./observableStakingScript"; +import { StakerInfo, Staking } from ".."; +import { networks, Psbt, Transaction } from "bitcoinjs-lib"; +export * from "./observableStakingScript"; +/** + * ObservableStaking is a class that provides an interface to create observable + * staking transactions for the Babylon Staking protocol. + * + * The class requires a network and staker information to create staking + * transactions. + * The staker information includes the staker's address and + * public key(without coordinates). + */ +export declare class ObservableStaking extends Staking { + params: ObservableVersionedStakingParams; + constructor(network: networks.Network, stakerInfo: StakerInfo, params: ObservableVersionedStakingParams, finalityProviderPkNoCoordHex: string, stakingTimelock: number); + /** + * Build the staking scripts for observable staking. + * This method overwrites the base method to include the OP_RETURN tag based + * on the tag provided in the parameters. + * + * @returns {ObservableStakingScripts} - The staking scripts for observable staking. + * @throws {StakingError} - If the scripts cannot be built. + */ + buildScripts(): ObservableStakingScripts; + /** + * Create a staking transaction for observable staking. + * This overwrites the method from the Staking class with the addtion + * of the + * 1. OP_RETURN tag in the staking scripts + * 2. lockHeight parameter + * + * @param {number} stakingAmountSat - The amount to stake in satoshis. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {TransactionResult} - An object containing the unsigned transaction, + * and fee + */ + createStakingTransaction(stakingAmountSat: number, inputUTXOs: UTXO[], feeRate: number): TransactionResult; + /** + * Create a staking psbt for observable staking. + * + * @param {Transaction} stakingTx - The staking transaction. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @returns {Psbt} - The psbt. + */ + toStakingPsbt(stakingTx: Transaction, inputUTXOs: UTXO[]): Psbt; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js new file mode 100644 index 0000000000..79f749f140 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js @@ -0,0 +1,121 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ObservableStaking = void 0; +const error_1 = require("../../error"); +const transactions_1 = require("../transactions"); +const btc_1 = require("../../utils/btc"); +const staking_1 = require("../../utils/staking"); +const observableStakingScript_1 = require("./observableStakingScript"); +const __1 = require(".."); +const psbt_1 = require("../psbt"); +__exportStar(require("./observableStakingScript"), exports); +/** + * ObservableStaking is a class that provides an interface to create observable + * staking transactions for the Babylon Staking protocol. + * + * The class requires a network and staker information to create staking + * transactions. + * The staker information includes the staker's address and + * public key(without coordinates). + */ +class ObservableStaking extends __1.Staking { + constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) { + super(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock); + if (!params.tag) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Observable staking parameters must include tag"); + } + if (!params.btcActivationHeight) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Observable staking parameters must include a positive activation height"); + } + // Override the staking parameters type to ObservableStakingParams + this.params = params; + } + /** + * Build the staking scripts for observable staking. + * This method overwrites the base method to include the OP_RETURN tag based + * on the tag provided in the parameters. + * + * @returns {ObservableStakingScripts} - The staking scripts for observable staking. + * @throws {StakingError} - If the scripts cannot be built. + */ + buildScripts() { + const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params; + // Create staking script data + let stakingScriptData; + try { + stakingScriptData = new observableStakingScript_1.ObservableStakingScriptData(Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"), [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")], (0, staking_1.toBuffers)(covenantNoCoordPks), covenantQuorum, this.stakingTimelock, unbondingTime, Buffer.from(tag, "hex")); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking script data"); + } + // Build scripts + let scripts; + try { + scripts = stakingScriptData.buildScripts(); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking scripts"); + } + return scripts; + } + /** + * Create a staking transaction for observable staking. + * This overwrites the method from the Staking class with the addtion + * of the + * 1. OP_RETURN tag in the staking scripts + * 2. lockHeight parameter + * + * @param {number} stakingAmountSat - The amount to stake in satoshis. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {TransactionResult} - An object containing the unsigned transaction, + * and fee + */ + createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) { + (0, staking_1.validateStakingTxInputData)(stakingAmountSat, this.stakingTimelock, this.params, inputUTXOs, feeRate); + const scripts = this.buildScripts(); + // Create the staking transaction + try { + const { transaction, fee } = (0, transactions_1.stakingTransaction)(scripts, stakingAmountSat, this.stakerInfo.address, inputUTXOs, this.network, feeRate, + // `lockHeight` is exclusive of the provided value. + // For example, if a Bitcoin height of X is provided, + // the transaction will be included starting from height X+1. + // https://learnmeabitcoin.com/technical/transaction/locktime/ + this.params.btcActivationHeight - 1); + return { + transaction, + fee, + }; + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned staking transaction"); + } + } + /** + * Create a staking psbt for observable staking. + * + * @param {Transaction} stakingTx - The staking transaction. + * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking + * transaction. + * @returns {Psbt} - The psbt. + */ + toStakingPsbt(stakingTx, inputUTXOs) { + return (0, psbt_1.stakingPsbt)(stakingTx, this.network, inputUTXOs, (0, btc_1.isTaproot)(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : undefined); + } +} +exports.ObservableStaking = ObservableStaking; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts new file mode 100644 index 0000000000..908669a846 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts @@ -0,0 +1,26 @@ +import { StakingScriptData, StakingScripts } from "../stakingScript"; +export interface ObservableStakingScripts extends StakingScripts { + dataEmbedScript: Buffer; +} +export declare class ObservableStakingScriptData extends StakingScriptData { + magicBytes: Buffer; + constructor(stakerKey: Buffer, finalityProviderKeys: Buffer[], covenantKeys: Buffer[], covenantThreshold: number, stakingTimelock: number, unbondingTimelock: number, magicBytes: Buffer); + /** + * Builds a data embed script for staking in the form: + * OP_RETURN || + * where serializedStakingData is the concatenation of: + * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock + * Note: Only a single finality provider key is supported for now in phase 1 + * @throws {Error} If the number of finality provider keys is not equal to 1. + * @returns {Buffer} The compiled data embed script. + */ + buildDataEmbedScript(): Buffer; + /** + * Builds the staking scripts. + * @returns {ObservableStakingScripts} The staking scripts that can be used to stake. + * contains the timelockScript, unbondingScript, slashingScript, + * unbondingTimelockScript, and dataEmbedScript. + * @throws {Error} If script data is invalid. + */ + buildScripts(): ObservableStakingScripts; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js new file mode 100644 index 0000000000..507ff4edc7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js @@ -0,0 +1,60 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ObservableStakingScriptData = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const stakingScript_1 = require("../stakingScript"); +class ObservableStakingScriptData extends stakingScript_1.StakingScriptData { + constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) { + super(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock); + if (!magicBytes) { + throw new Error("Missing required input values"); + } + // check that the magic bytes are 4 in length + if (magicBytes.length != stakingScript_1.MAGIC_BYTES_LEN) { + throw new Error("Invalid script data provided"); + } + this.magicBytes = magicBytes; + } + /** + * Builds a data embed script for staking in the form: + * OP_RETURN || + * where serializedStakingData is the concatenation of: + * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock + * Note: Only a single finality provider key is supported for now in phase 1 + * @throws {Error} If the number of finality provider keys is not equal to 1. + * @returns {Buffer} The compiled data embed script. + */ + buildDataEmbedScript() { + // Only accept a single finality provider key for now + if (this.finalityProviderKeys.length != 1) { + throw new Error("Only a single finality provider key is supported"); + } + // 1 byte for version + const version = Buffer.alloc(1); + version.writeUInt8(0); + // 2 bytes for staking time + const stakingTimeLock = Buffer.alloc(2); + // big endian + stakingTimeLock.writeUInt16BE(this.stakingTimeLock); + const serializedStakingData = Buffer.concat([ + this.magicBytes, + version, + this.stakerKey, + this.finalityProviderKeys[0], + stakingTimeLock, + ]); + return bitcoinjs_lib_1.script.compile([bitcoinjs_lib_1.opcodes.OP_RETURN, serializedStakingData]); + } + /** + * Builds the staking scripts. + * @returns {ObservableStakingScripts} The staking scripts that can be used to stake. + * contains the timelockScript, unbondingScript, slashingScript, + * unbondingTimelockScript, and dataEmbedScript. + * @throws {Error} If script data is invalid. + */ + buildScripts() { + const scripts = super.buildScripts(); + return Object.assign(Object.assign({}, scripts), { dataEmbedScript: this.buildDataEmbedScript() }); + } +} +exports.ObservableStakingScriptData = ObservableStakingScriptData; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts new file mode 100644 index 0000000000..c033fb8d22 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts @@ -0,0 +1,20 @@ +import { Psbt, Transaction, networks } from "bitcoinjs-lib"; +import { UTXO } from "../types/UTXO"; +/** + * Convert a staking transaction to a PSBT. + * + * @param {Transaction} stakingTx - The staking transaction to convert to PSBT. + * @param {networks.Network} network - The network to use for the PSBT. + * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking + * transaction. + * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional) + * @returns {Psbt} - The PSBT for the staking transaction. + * @throws {Error} If unable to create PSBT from transaction + */ +export declare const stakingPsbt: (stakingTx: Transaction, network: networks.Network, inputUTXOs: UTXO[], publicKeyNoCoord?: Buffer) => Psbt; +export declare const unbondingPsbt: (scripts: { + unbondingScript: Buffer; + timelockScript: Buffer; + slashingScript: Buffer; + unbondingTimelockScript: Buffer; +}, unbondingTx: Transaction, stakingTx: Transaction, network: networks.Network) => Psbt; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js new file mode 100644 index 0000000000..8c0c34b3e5 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js @@ -0,0 +1,113 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.unbondingPsbt = exports.stakingPsbt = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const internalPubkey_1 = require("../constants/internalPubkey"); +const keys_1 = require("../constants/keys"); +const transaction_1 = require("../constants/transaction"); +const staking_1 = require("../utils/staking"); +const findInputUTXO_1 = require("../utils/utxo/findInputUTXO"); +const getPsbtInputFields_1 = require("../utils/utxo/getPsbtInputFields"); +/** + * Convert a staking transaction to a PSBT. + * + * @param {Transaction} stakingTx - The staking transaction to convert to PSBT. + * @param {networks.Network} network - The network to use for the PSBT. + * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking + * transaction. + * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional) + * @returns {Psbt} - The PSBT for the staking transaction. + * @throws {Error} If unable to create PSBT from transaction + */ +const stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => { + if (publicKeyNoCoord && publicKeyNoCoord.length !== keys_1.NO_COORD_PK_BYTE_LENGTH) { + throw new Error("Invalid public key"); + } + const psbt = new bitcoinjs_lib_1.Psbt({ network }); + if (stakingTx.version !== undefined) + psbt.setVersion(stakingTx.version); + if (stakingTx.locktime !== undefined) + psbt.setLocktime(stakingTx.locktime); + stakingTx.ins.forEach((input) => { + const inputUTXO = (0, findInputUTXO_1.findInputUTXO)(inputUTXOs, input); + const psbtInputData = (0, getPsbtInputFields_1.getPsbtInputFields)(inputUTXO, publicKeyNoCoord); + psbt.addInput(Object.assign({ hash: input.hash, index: input.index, sequence: input.sequence }, psbtInputData)); + }); + stakingTx.outs.forEach((o) => { + psbt.addOutput({ script: o.script, value: o.value }); + }); + return psbt; +}; +exports.stakingPsbt = stakingPsbt; +const unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => { + if (unbondingTx.outs.length !== 1) { + throw new Error("Unbonding transaction must have exactly one output"); + } + if (unbondingTx.ins.length !== 1) { + throw new Error("Unbonding transaction must have exactly one input"); + } + validateUnbondingOutput(scripts, unbondingTx, network); + const psbt = new bitcoinjs_lib_1.Psbt({ network }); + if (unbondingTx.version !== undefined) { + psbt.setVersion(unbondingTx.version); + } + if (unbondingTx.locktime !== undefined) { + psbt.setLocktime(unbondingTx.locktime); + } + const input = unbondingTx.ins[0]; + const outputIndex = input.index; + // Build input tapleaf script + const inputScriptTree = [ + { output: scripts.slashingScript }, + [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], + ]; + // This is the tapleaf we are actually spending + const inputRedeem = { + output: scripts.unbondingScript, + redeemVersion: transaction_1.REDEEM_VERSION, + }; + // Create a P2TR payment that includes scriptTree + redeem + const p2tr = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree: inputScriptTree, + redeem: inputRedeem, + network, + }); + const inputTapLeafScript = { + leafVersion: inputRedeem.redeemVersion, + script: inputRedeem.output, + controlBlock: p2tr.witness[p2tr.witness.length - 1], + }; + psbt.addInput({ + hash: input.hash, + index: input.index, + sequence: input.sequence, + tapInternalKey: internalPubkey_1.internalPubkey, + witnessUtxo: { + value: stakingTx.outs[outputIndex].value, + script: stakingTx.outs[outputIndex].script, + }, + tapLeafScript: [inputTapLeafScript], + }); + psbt.addOutput({ + script: unbondingTx.outs[0].script, + value: unbondingTx.outs[0].value, + }); + return psbt; +}; +exports.unbondingPsbt = unbondingPsbt; +/** + * Validate the unbonding output for a given unbonding transaction. + * + * @param {Object} scripts - The scripts to use for the unbonding output. + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {networks.Network} network - The network to use for the unbonding output. + */ +const validateUnbondingOutput = (scripts, unbondingTx, network) => { + const unbondingOutputInfo = (0, staking_1.deriveUnbondingOutputInfo)(scripts, network); + if (unbondingOutputInfo.scriptPubKey.toString("hex") !== + unbondingTx.outs[0].script.toString("hex")) { + throw new Error("Unbonding output script does not match the expected" + + " script while building psbt"); + } +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts new file mode 100644 index 0000000000..1eb07d97ca --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts @@ -0,0 +1,96 @@ +export declare const MAGIC_BYTES_LEN = 4; +export interface StakingScripts { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; + unbondingTimelockScript: Buffer; +} +export declare class StakingScriptData { + stakerKey: Buffer; + finalityProviderKeys: Buffer[]; + covenantKeys: Buffer[]; + covenantThreshold: number; + stakingTimeLock: number; + unbondingTimeLock: number; + constructor(stakerKey: Buffer, finalityProviderKeys: Buffer[], covenantKeys: Buffer[], covenantThreshold: number, stakingTimelock: number, unbondingTimelock: number); + /** + * Validates the staking script. + * @returns {boolean} Returns true if the staking script is valid, otherwise false. + */ + validate(): boolean; + /** + * Builds a timelock script. + * @param timelock - The timelock value to encode in the script. + * @returns {Buffer} containing the compiled timelock script. + */ + buildTimelockScript(timelock: number): Buffer; + /** + * Builds the staking timelock script. + * Only holder of private key for given pubKey can spend after relative lock time + * Creates the timelock script in the form: + * + * OP_CHECKSIGVERIFY + * + * OP_CHECKSEQUENCEVERIFY + * @returns {Buffer} The staking timelock script. + */ + buildStakingTimelockScript(): Buffer; + /** + * Builds the unbonding timelock script. + * Creates the unbonding timelock script in the form: + * + * OP_CHECKSIGVERIFY + * + * OP_CHECKSEQUENCEVERIFY + * @returns {Buffer} The unbonding timelock script. + */ + buildUnbondingTimelockScript(): Buffer; + /** + * Builds the unbonding script in the form: + * buildSingleKeyScript(stakerPk, true) || + * buildMultiKeyScript(covenantPks, covenantThreshold, false) + * || means combining the scripts + * @returns {Buffer} The unbonding script. + */ + buildUnbondingScript(): Buffer; + /** + * Builds the slashing script for staking in the form: + * buildSingleKeyScript(stakerPk, true) || + * buildMultiKeyScript(finalityProviderPKs, 1, true) || + * buildMultiKeyScript(covenantPks, covenantThreshold, false) + * || means combining the scripts + * The slashing script is a combination of single-key and multi-key scripts. + * The single-key script is used for staker key verification. + * The multi-key script is used for finality provider key verification and covenant key verification. + * @returns {Buffer} The slashing script as a Buffer. + */ + buildSlashingScript(): Buffer; + /** + * Builds the staking scripts. + * @returns {StakingScripts} The staking scripts. + */ + buildScripts(): StakingScripts; + /** + * Builds a single key script in the form: + * buildSingleKeyScript creates a single key script + * OP_CHECKSIGVERIFY (if withVerify is true) + * OP_CHECKSIG (if withVerify is false) + * @param pk - The public key buffer. + * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode. + * @returns The compiled script buffer. + */ + buildSingleKeyScript(pk: Buffer, withVerify: boolean): Buffer; + /** + * Builds a multi-key script in the form: + * OP_CHEKCSIG OP_CHECKSIGADD OP_CHECKSIGADD ... OP_CHECKSIGADD OP_NUMEQUAL + * OP_NUMEQUALVERIFY> + * It validates whether provided keys are unique and the threshold is not greater than number of keys + * If there is only one key provided it will return single key sig script + * @param pks - An array of public keys. + * @param threshold - The required number of valid signers. + * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode. + * @returns The compiled multi-key script as a Buffer. + * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided. + */ + buildMultiKeyScript(pks: Buffer[], threshold: number, withVerify: boolean): Buffer; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js new file mode 100644 index 0000000000..7b6e652baa --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js @@ -0,0 +1,257 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StakingScriptData = exports.MAGIC_BYTES_LEN = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const keys_1 = require("../constants/keys"); +exports.MAGIC_BYTES_LEN = 4; +// StakingScriptData is a class that holds the data required for the BTC Staking Script +// and exposes methods for converting it into useful formats +class StakingScriptData { + constructor( + // The `stakerKey` is the public key of the staker without the coordinate bytes. + stakerKey, + // A list of public keys without the coordinate bytes corresponding to the finality providers + // the stake will be delegated to. + // Currently, Babylon does not support restaking, so this should contain only a single item. + finalityProviderKeys, + // A list of the public keys without the coordinate bytes corresponding to + // the covenant emulators. + // This is a parameter of the Babylon system and should be retrieved from there. + covenantKeys, + // The number of covenant emulator signatures required for a transaction + // to be valid. + // This is a parameter of the Babylon system and should be retrieved from there. + covenantThreshold, + // The staking period denoted as a number of BTC blocks. + stakingTimelock, + // The unbonding period denoted as a number of BTC blocks. + // This value should be more than equal than the minimum unbonding time of the + // Babylon system. + unbondingTimelock) { + if (!stakerKey || + !finalityProviderKeys || + !covenantKeys || + !covenantThreshold || + !stakingTimelock || + !unbondingTimelock) { + throw new Error("Missing required input values"); + } + this.stakerKey = stakerKey; + this.finalityProviderKeys = finalityProviderKeys; + this.covenantKeys = covenantKeys; + this.covenantThreshold = covenantThreshold; + this.stakingTimeLock = stakingTimelock; + this.unbondingTimeLock = unbondingTimelock; + // Run the validate method to check if the provided script data is valid + if (!this.validate()) { + throw new Error("Invalid script data provided"); + } + } + /** + * Validates the staking script. + * @returns {boolean} Returns true if the staking script is valid, otherwise false. + */ + validate() { + // check that staker key is the correct length + if (this.stakerKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH) { + return false; + } + // check that finalityProvider keys are the correct length + if (this.finalityProviderKeys.some((finalityProviderKey) => finalityProviderKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { + return false; + } + // check that covenant keys are the correct length + if (this.covenantKeys.some((covenantKey) => covenantKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { + return false; + } + // Check whether we have any duplicate keys + const allPks = [ + this.stakerKey, + ...this.finalityProviderKeys, + ...this.covenantKeys, + ]; + const allPksSet = new Set(allPks); + if (allPks.length !== allPksSet.size) { + return false; + } + // check that the threshold is above 0 and less than or equal to + // the size of the covenant emulators set + if (this.covenantThreshold <= 0 || + this.covenantThreshold > this.covenantKeys.length) { + return false; + } + // check that maximum value for staking time is not greater than uint16 and above 0 + if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) { + return false; + } + // check that maximum value for unbonding time is not greater than uint16 and above 0 + if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) { + return false; + } + return true; + } + // The staking script allows for multiple finality provider public keys + // to support (re)stake to multiple finality providers + // Covenant members are going to have multiple keys + /** + * Builds a timelock script. + * @param timelock - The timelock value to encode in the script. + * @returns {Buffer} containing the compiled timelock script. + */ + buildTimelockScript(timelock) { + return bitcoinjs_lib_1.script.compile([ + this.stakerKey, + bitcoinjs_lib_1.opcodes.OP_CHECKSIGVERIFY, + bitcoinjs_lib_1.script.number.encode(timelock), + bitcoinjs_lib_1.opcodes.OP_CHECKSEQUENCEVERIFY, + ]); + } + /** + * Builds the staking timelock script. + * Only holder of private key for given pubKey can spend after relative lock time + * Creates the timelock script in the form: + * + * OP_CHECKSIGVERIFY + * + * OP_CHECKSEQUENCEVERIFY + * @returns {Buffer} The staking timelock script. + */ + buildStakingTimelockScript() { + return this.buildTimelockScript(this.stakingTimeLock); + } + /** + * Builds the unbonding timelock script. + * Creates the unbonding timelock script in the form: + * + * OP_CHECKSIGVERIFY + * + * OP_CHECKSEQUENCEVERIFY + * @returns {Buffer} The unbonding timelock script. + */ + buildUnbondingTimelockScript() { + return this.buildTimelockScript(this.unbondingTimeLock); + } + /** + * Builds the unbonding script in the form: + * buildSingleKeyScript(stakerPk, true) || + * buildMultiKeyScript(covenantPks, covenantThreshold, false) + * || means combining the scripts + * @returns {Buffer} The unbonding script. + */ + buildUnbondingScript() { + return Buffer.concat([ + this.buildSingleKeyScript(this.stakerKey, true), + this.buildMultiKeyScript(this.covenantKeys, this.covenantThreshold, false), + ]); + } + /** + * Builds the slashing script for staking in the form: + * buildSingleKeyScript(stakerPk, true) || + * buildMultiKeyScript(finalityProviderPKs, 1, true) || + * buildMultiKeyScript(covenantPks, covenantThreshold, false) + * || means combining the scripts + * The slashing script is a combination of single-key and multi-key scripts. + * The single-key script is used for staker key verification. + * The multi-key script is used for finality provider key verification and covenant key verification. + * @returns {Buffer} The slashing script as a Buffer. + */ + buildSlashingScript() { + return Buffer.concat([ + this.buildSingleKeyScript(this.stakerKey, true), + this.buildMultiKeyScript(this.finalityProviderKeys, + // The threshold is always 1 as we only need one + // finalityProvider signature to perform slashing + // (only one finalityProvider performs an offence) + 1, + // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end + true), + this.buildMultiKeyScript(this.covenantKeys, this.covenantThreshold, + // No need to add verify since covenants are at the end of the script + false), + ]); + } + /** + * Builds the staking scripts. + * @returns {StakingScripts} The staking scripts. + */ + buildScripts() { + return { + timelockScript: this.buildStakingTimelockScript(), + unbondingScript: this.buildUnbondingScript(), + slashingScript: this.buildSlashingScript(), + unbondingTimelockScript: this.buildUnbondingTimelockScript(), + }; + } + // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality + // for creating Bitcoin scripts for the unbonding script and the slashing script + /** + * Builds a single key script in the form: + * buildSingleKeyScript creates a single key script + * OP_CHECKSIGVERIFY (if withVerify is true) + * OP_CHECKSIG (if withVerify is false) + * @param pk - The public key buffer. + * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode. + * @returns The compiled script buffer. + */ + buildSingleKeyScript(pk, withVerify) { + // Check public key length + if (pk.length != keys_1.NO_COORD_PK_BYTE_LENGTH) { + throw new Error("Invalid key length"); + } + return bitcoinjs_lib_1.script.compile([ + pk, + withVerify ? bitcoinjs_lib_1.opcodes.OP_CHECKSIGVERIFY : bitcoinjs_lib_1.opcodes.OP_CHECKSIG, + ]); + } + /** + * Builds a multi-key script in the form: + * OP_CHEKCSIG OP_CHECKSIGADD OP_CHECKSIGADD ... OP_CHECKSIGADD OP_NUMEQUAL + * OP_NUMEQUALVERIFY> + * It validates whether provided keys are unique and the threshold is not greater than number of keys + * If there is only one key provided it will return single key sig script + * @param pks - An array of public keys. + * @param threshold - The required number of valid signers. + * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode. + * @returns The compiled multi-key script as a Buffer. + * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided. + */ + buildMultiKeyScript(pks, threshold, withVerify) { + // Verify that pks is not empty + if (!pks || pks.length === 0) { + throw new Error("No keys provided"); + } + // Check buffer object have expected lengths like checking pks.length + if (pks.some((pk) => pk.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { + throw new Error("Invalid key length"); + } + // Verify that threshold <= len(pks) + if (threshold > pks.length) { + throw new Error("Required number of valid signers is greater than number of provided keys"); + } + if (pks.length === 1) { + return this.buildSingleKeyScript(pks[0], withVerify); + } + // keys must be sorted + const sortedPks = [...pks].sort(Buffer.compare); + // verify there are no duplicates + for (let i = 0; i < sortedPks.length - 1; ++i) { + if (sortedPks[i].equals(sortedPks[i + 1])) { + throw new Error("Duplicate keys provided"); + } + } + const scriptElements = [sortedPks[0], bitcoinjs_lib_1.opcodes.OP_CHECKSIG]; + for (let i = 1; i < sortedPks.length; i++) { + scriptElements.push(sortedPks[i]); + scriptElements.push(bitcoinjs_lib_1.opcodes.OP_CHECKSIGADD); + } + scriptElements.push(bitcoinjs_lib_1.script.number.encode(threshold)); + if (withVerify) { + scriptElements.push(bitcoinjs_lib_1.opcodes.OP_NUMEQUALVERIFY); + } + else { + scriptElements.push(bitcoinjs_lib_1.opcodes.OP_NUMEQUAL); + } + return bitcoinjs_lib_1.script.compile(scriptElements); + } +} +exports.StakingScriptData = StakingScriptData; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts new file mode 100644 index 0000000000..68afd0e848 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts @@ -0,0 +1,191 @@ +import { Psbt, Transaction, networks } from "bitcoinjs-lib"; +import { UTXO } from "../types/UTXO"; +import { PsbtResult, TransactionResult } from "../types/transaction"; +import { CovenantSignature } from "../types/covenantSignatures"; +/** + * Constructs an unsigned BTC Staking transaction in psbt format. + * + * Outputs: + * - psbt: + * - The first output corresponds to the staking script with the specified amount. + * - The second output corresponds to the change from spending the amount and the transaction fee. + * - If a data embed script is provided, it will be added as the second output, and the fee will be the third output. + * - fee: The total fee amount for the transaction. + * + * Inputs: + * - scripts: + * - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types. + * - dataEmbedScript: Optional data embed script. + * - amount: Amount to stake. + * - changeAddress: Address to send the change to. + * - inputUTXOs: All available UTXOs from the wallet. + * - network: Bitcoin network. + * - feeRate: Fee rate in satoshis per byte. + * - publicKeyNoCoord: Public key if the wallet is in taproot mode. + * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height). + * + * @param {Object} scripts - Scripts used to construct the taproot output. + * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript. + * @param {number} amount - The amount to stake. + * @param {string} changeAddress - The address to send the change to. + * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate in satoshis per byte. + * @param {number} [lockHeight] - The optional block height locktime. + * @returns {TransactionResult} - An object containing the unsigned transaction and fee + * @throws Will throw an error if the amount or fee rate is less than or equal + * to 0, if the change address is invalid, or if the public key is invalid. + */ +export declare function stakingTransaction(scripts: { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; + dataEmbedScript?: Buffer; +}, amount: number, changeAddress: string, inputUTXOs: UTXO[], network: networks.Network, feeRate: number, lockHeight?: number): TransactionResult; +/** + * Constructs a withdrawal transaction for manually unbonded delegation. + * + * This transaction spends the unbonded output from the staking transaction. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - unbondingTimelockScript: Script for the unbonding timelock condition. + * - slashingScript: Script for the slashing condition. + * - unbondingTx: The unbonding transaction. + * - withdrawalAddress: The address to send the withdrawn funds to. + * - network: The Bitcoin network. + * - feeRate: The fee rate for the transaction in satoshis per byte. + * + * Returns: + * - psbt: The partially signed transaction (PSBT). + * + * @param {Object} scripts - The scripts used in the transaction. + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +export declare function withdrawEarlyUnbondedTransaction(scripts: { + unbondingTimelockScript: Buffer; + slashingScript: Buffer; +}, unbondingTx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number): PsbtResult; +/** + * Constructs a withdrawal transaction for naturally unbonded delegation. + * + * This transaction spends the unbonded output from the staking transaction when the timelock has expired. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - timelockScript: Script for the timelock condition. + * - slashingScript: Script for the slashing condition. + * - unbondingScript: Script for the unbonding condition. + * - tx: The original staking transaction. + * - withdrawalAddress: The address to send the withdrawn funds to. + * - network: The Bitcoin network. + * - feeRate: The fee rate for the transaction in satoshis per byte. + * - outputIndex: The index of the output to be spent in the original transaction (default is 0). + * + * Returns: + * - psbt: The partially signed transaction (PSBT). + * + * @param {Object} scripts - The scripts used in the transaction. + * @param {Transaction} tx - The original staking transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +export declare function withdrawTimelockUnbondedTransaction(scripts: { + timelockScript: Buffer; + slashingScript: Buffer; + unbondingScript: Buffer; +}, tx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number, outputIndex?: number): PsbtResult; +/** + * Constructs a withdrawal transaction for a slashing transaction. + * + * This transaction spends the output from the slashing transaction. + * + * @param {Object} scripts - The unbondingTimelockScript + * We use the unbonding timelock script as the timelock of the slashing transaction. + * This is due to slashing tx timelock is the same as the unbonding timelock. + * @param {Transaction} slashingTx - The slashing transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @param {number} outputIndex - The index of the output to be spent in the original transaction. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +export declare function withdrawSlashingTransaction(scripts: { + unbondingTimelockScript: Buffer; +}, slashingTx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number, outputIndex: number): PsbtResult; +/** + * Constructs a slashing transaction for a staking output without prior unbonding. + * + * This transaction spends the staking output of the staking transaction and distributes the funds + * according to the specified slashing rate. + * + * Outputs: + * - The first output sends `input * slashing_rate` funds to the slashing address. + * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - slashingScript: Script for the slashing condition. + * - timelockScript: Script for the timelock condition. + * - unbondingScript: Script for the unbonding condition. + * - unbondingTimelockScript: Script for the unbonding timelock condition. + * - transaction: The original staking 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. + * @param {Transaction} stakingTransaction - The original staking transaction. + * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. + * @param {number} slashingRate - The rate at which the funds are slashed. + * @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. + * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). + */ +export declare function slashTimelockUnbondedTransaction(scripts: { + slashingScript: Buffer; + timelockScript: Buffer; + unbondingScript: Buffer; + unbondingTimelockScript: Buffer; +}, stakingTransaction: Transaction, slashingPkScriptHex: string, slashingRate: number, minimumFee: number, network: networks.Network, outputIndex?: number): { + psbt: Psbt; +}; +/** + * Constructs a slashing transaction for an early unbonded transaction. + * + * This transaction spends the staking output of the staking transaction and distributes the funds + * according to the specified slashing rate. + * + * Transaction outputs: + * - The first output sends `input * slashing_rate` funds to the slashing address. + * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. + * + * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {string} slashingPkScriptHex - The public key script 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. + * @param {networks.Network} network - The Bitcoin network. + * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). + */ +export declare function slashEarlyUnbondedTransaction(scripts: { + slashingScript: Buffer; + unbondingTimelockScript: Buffer; +}, unbondingTx: Transaction, slashingPkScriptHex: string, slashingRate: number, minimumSlashingFee: number, network: networks.Network): { + psbt: Psbt; +}; +export declare function unbondingTransaction(scripts: { + unbondingTimelockScript: Buffer; + slashingScript: Buffer; +}, stakingTx: Transaction, unbondingFee: number, network: networks.Network, outputIndex?: number): TransactionResult; +export declare const createCovenantWitness: (originalWitness: Buffer[], paramsCovenants: Buffer[], covenantSigs: CovenantSignature[], covenantQuorum: number) => Buffer[]; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js new file mode 100644 index 0000000000..3056092f04 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js @@ -0,0 +1,515 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createCovenantWitness = void 0; +exports.stakingTransaction = stakingTransaction; +exports.withdrawEarlyUnbondedTransaction = withdrawEarlyUnbondedTransaction; +exports.withdrawTimelockUnbondedTransaction = withdrawTimelockUnbondedTransaction; +exports.withdrawSlashingTransaction = withdrawSlashingTransaction; +exports.slashTimelockUnbondedTransaction = slashTimelockUnbondedTransaction; +exports.slashEarlyUnbondedTransaction = slashEarlyUnbondedTransaction; +exports.unbondingTransaction = unbondingTransaction; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const dustSat_1 = require("../constants/dustSat"); +const internalPubkey_1 = require("../constants/internalPubkey"); +const btc_1 = require("../utils/btc"); +const fee_1 = require("../utils/fee"); +const utils_1 = require("../utils/fee/utils"); +const staking_1 = require("../utils/staking"); +const psbt_1 = require("../constants/psbt"); +const transaction_1 = require("../constants/transaction"); +// https://bips.xyz/370 +const BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 500000000; +const BTC_SLASHING_FRACTION_DIGITS = 4; +/** + * Constructs an unsigned BTC Staking transaction in psbt format. + * + * Outputs: + * - psbt: + * - The first output corresponds to the staking script with the specified amount. + * - The second output corresponds to the change from spending the amount and the transaction fee. + * - If a data embed script is provided, it will be added as the second output, and the fee will be the third output. + * - fee: The total fee amount for the transaction. + * + * Inputs: + * - scripts: + * - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types. + * - dataEmbedScript: Optional data embed script. + * - amount: Amount to stake. + * - changeAddress: Address to send the change to. + * - inputUTXOs: All available UTXOs from the wallet. + * - network: Bitcoin network. + * - feeRate: Fee rate in satoshis per byte. + * - publicKeyNoCoord: Public key if the wallet is in taproot mode. + * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height). + * + * @param {Object} scripts - Scripts used to construct the taproot output. + * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript. + * @param {number} amount - The amount to stake. + * @param {string} changeAddress - The address to send the change to. + * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate in satoshis per byte. + * @param {number} [lockHeight] - The optional block height locktime. + * @returns {TransactionResult} - An object containing the unsigned transaction and fee + * @throws Will throw an error if the amount or fee rate is less than or equal + * to 0, if the change address is invalid, or if the public key is invalid. + */ +function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) { + // Check that amount and fee are bigger than 0 + if (amount <= 0 || feeRate <= 0) { + throw new Error("Amount and fee rate must be bigger than 0"); + } + // Check whether the change address is a valid Bitcoin address. + if (!(0, btc_1.isValidBitcoinAddress)(changeAddress, network)) { + throw new Error("Invalid change address"); + } + // Build outputs and estimate the fee + const stakingOutputs = (0, staking_1.buildStakingTransactionOutputs)(scripts, network, amount); + const { selectedUTXOs, fee } = (0, fee_1.getStakingTxInputUTXOsAndFees)(inputUTXOs, amount, feeRate, stakingOutputs); + const tx = new bitcoinjs_lib_1.Transaction(); + tx.version = psbt_1.TRANSACTION_VERSION; + for (let i = 0; i < selectedUTXOs.length; ++i) { + const input = selectedUTXOs[i]; + tx.addInput((0, btc_1.transactionIdToHash)(input.txid), input.vout, psbt_1.NON_RBF_SEQUENCE); + } + stakingOutputs.forEach((o) => { + tx.addOutput(o.scriptPubKey, o.value); + }); + // Add a change output only if there's any amount leftover from the inputs + const inputsSum = (0, utils_1.inputValueSum)(selectedUTXOs); + // Check if the change amount is above the dust limit, and if so, add it as a change output + if (inputsSum - (amount + fee) > dustSat_1.BTC_DUST_SAT) { + tx.addOutput(bitcoinjs_lib_1.address.toOutputScript(changeAddress, network), inputsSum - (amount + fee)); + } + // Set the locktime field if provided. If not provided, the locktime will be set to 0 by default + // Only height based locktime is supported + if (lockHeight) { + if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) { + throw new Error("Invalid lock height"); + } + tx.locktime = lockHeight; + } + return { + transaction: tx, + fee, + }; +} +/** + * Constructs a withdrawal transaction for manually unbonded delegation. + * + * This transaction spends the unbonded output from the staking transaction. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - unbondingTimelockScript: Script for the unbonding timelock condition. + * - slashingScript: Script for the slashing condition. + * - unbondingTx: The unbonding transaction. + * - withdrawalAddress: The address to send the withdrawn funds to. + * - network: The Bitcoin network. + * - feeRate: The fee rate for the transaction in satoshis per byte. + * + * Returns: + * - psbt: The partially signed transaction (PSBT). + * + * @param {Object} scripts - The scripts used in the transaction. + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) { + const scriptTree = [ + { + output: scripts.slashingScript, + }, + { output: scripts.unbondingTimelockScript }, + ]; + return withdrawalTransaction({ + timelockScript: scripts.unbondingTimelockScript, + }, scriptTree, unbondingTx, withdrawalAddress, network, feeRate, 0); +} +/** + * Constructs a withdrawal transaction for naturally unbonded delegation. + * + * This transaction spends the unbonded output from the staking transaction when the timelock has expired. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - timelockScript: Script for the timelock condition. + * - slashingScript: Script for the slashing condition. + * - unbondingScript: Script for the unbonding condition. + * - tx: The original staking transaction. + * - withdrawalAddress: The address to send the withdrawn funds to. + * - network: The Bitcoin network. + * - feeRate: The fee rate for the transaction in satoshis per byte. + * - outputIndex: The index of the output to be spent in the original transaction (default is 0). + * + * Returns: + * - psbt: The partially signed transaction (PSBT). + * + * @param {Object} scripts - The scripts used in the transaction. + * @param {Transaction} tx - The original staking transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { + const scriptTree = [ + { + output: scripts.slashingScript, + }, + [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], + ]; + return withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex); +} +/** + * Constructs a withdrawal transaction for a slashing transaction. + * + * This transaction spends the output from the slashing transaction. + * + * @param {Object} scripts - The unbondingTimelockScript + * We use the unbonding timelock script as the timelock of the slashing transaction. + * This is due to slashing tx timelock is the same as the unbonding timelock. + * @param {Transaction} slashingTx - The slashing transaction. + * @param {string} withdrawalAddress - The address to send the withdrawn funds to. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. + * @param {number} outputIndex - The index of the output to be spent in the original transaction. + * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). + */ +function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) { + const scriptTree = { output: scripts.unbondingTimelockScript }; + return withdrawalTransaction({ + timelockScript: scripts.unbondingTimelockScript, + }, scriptTree, slashingTx, withdrawalAddress, network, feeRate, outputIndex); +} +// withdrawalTransaction generates a transaction that +// spends the staking output of the staking transaction +function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { + // Check that withdrawal feeRate is bigger than 0 + if (feeRate <= 0) { + throw new Error("Withdrawal feeRate must be bigger than 0"); + } + // Check that outputIndex is bigger or equal to 0 + if (outputIndex < 0) { + throw new Error("Output index must be bigger or equal to 0"); + } + // position of time in the timelock script + const timePosition = 2; + const decompiled = bitcoinjs_lib_1.script.decompile(scripts.timelockScript); + if (!decompiled) { + throw new Error("Timelock script is not valid"); + } + let timelock = 0; + // if the timelock is a buffer, it means it's a number bigger than 16 blocks + if (typeof decompiled[timePosition] !== "number") { + const timeBuffer = decompiled[timePosition]; + timelock = bitcoinjs_lib_1.script.number.decode(timeBuffer); + } + else { + // in case timelock is <= 16 it will be a number, not a buffer + const wrap = decompiled[timePosition] % 16; + timelock = wrap === 0 ? 16 : wrap; + } + const redeem = { + output: scripts.timelockScript, + redeemVersion: transaction_1.REDEEM_VERSION, + }; + const p2tr = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree, + redeem, + network, + }); + const tapLeafScript = { + leafVersion: redeem.redeemVersion, + script: redeem.output, + controlBlock: p2tr.witness[p2tr.witness.length - 1], + }; + const psbt = new bitcoinjs_lib_1.Psbt({ network }); + // only transactions with version 2 can trigger OP_CHECKSEQUENCEVERIFY + // https://github.com/btcsuite/btcd/blob/master/txscript/opcode.go#L1174 + psbt.setVersion(psbt_1.TRANSACTION_VERSION); + psbt.addInput({ + hash: tx.getHash(), + index: outputIndex, + tapInternalKey: internalPubkey_1.internalPubkey, + witnessUtxo: { + value: tx.outs[outputIndex].value, + script: tx.outs[outputIndex].script, + }, + tapLeafScript: [tapLeafScript], + sequence: timelock, + }); + const estimatedFee = (0, fee_1.getWithdrawTxFee)(feeRate); + const outputValue = tx.outs[outputIndex].value - estimatedFee; + if (outputValue < 0) { + throw new Error("Not enough funds to cover the fee for withdrawal transaction"); + } + if (outputValue < dustSat_1.BTC_DUST_SAT) { + throw new Error("Output value is less than dust limit"); + } + psbt.addOutput({ + address: withdrawalAddress, + value: outputValue, + }); + // Withdraw transaction has no time-based restrictions and can be included + // in the next block immediately. + psbt.setLocktime(0); + return { + psbt, + fee: estimatedFee, + }; +} +/** + * Constructs a slashing transaction for a staking output without prior unbonding. + * + * This transaction spends the staking output of the staking transaction and distributes the funds + * according to the specified slashing rate. + * + * Outputs: + * - The first output sends `input * slashing_rate` funds to the slashing address. + * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. + * + * Inputs: + * - scripts: Scripts used to construct the taproot output. + * - slashingScript: Script for the slashing condition. + * - timelockScript: Script for the timelock condition. + * - unbondingScript: Script for the unbonding condition. + * - unbondingTimelockScript: Script for the unbonding timelock condition. + * - transaction: The original staking 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. + * @param {Transaction} stakingTransaction - The original staking transaction. + * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. + * @param {number} slashingRate - The rate at which the funds are slashed. + * @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. + * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). + */ +function slashTimelockUnbondedTransaction(scripts, stakingTransaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { + const slashingScriptTree = [ + { + output: scripts.slashingScript, + }, + [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], + ]; + return slashingTransaction({ + unbondingTimelockScript: scripts.unbondingTimelockScript, + slashingScript: scripts.slashingScript, + }, slashingScriptTree, stakingTransaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex); +} +/** + * Constructs a slashing transaction for an early unbonded transaction. + * + * This transaction spends the staking output of the staking transaction and distributes the funds + * according to the specified slashing rate. + * + * Transaction outputs: + * - The first output sends `input * slashing_rate` funds to the slashing address. + * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. + * + * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript + * @param {Transaction} unbondingTx - The unbonding transaction. + * @param {string} slashingPkScriptHex - The public key script 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. + * @param {networks.Network} network - The Bitcoin network. + * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). + */ +function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) { + const unbondingScriptTree = [ + { + output: scripts.slashingScript, + }, + { + output: scripts.unbondingTimelockScript, + }, + ]; + return slashingTransaction({ + unbondingTimelockScript: scripts.unbondingTimelockScript, + slashingScript: scripts.slashingScript, + }, unbondingScriptTree, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network, 0); +} +/** + * Constructs a slashing transaction for an on-demand unbonding. + * + * This transaction spends the staking output of the staking transaction and distributes the funds + * according to the specified slashing rate. + * + * Transaction outputs: + * - The first output sends `input * slashing_rate` funds to the slashing address. + * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. + * + * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript + * @param {Transaction} transaction - The original staking/unbonding transaction. + * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. + * @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. + * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). + */ +function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { + // Check that slashing rate and minimum fee are 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(BTC_SLASHING_FRACTION_DIGITS)); + // 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 || !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 = { + output: scripts.slashingScript, + redeemVersion: transaction_1.REDEEM_VERSION, + }; + const p2tr = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree, + redeem, + network, + }); + const tapLeafScript = { + leafVersion: redeem.redeemVersion, + script: redeem.output, + 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.round(stakingAmount * slashingRate); + // Compute the slashing output + const slashingOutput = Buffer.from(slashingPkScriptHex, "hex"); + // If OP_RETURN is not included, the slashing amount must be greater than the + // dust limit. + if (bitcoinjs_lib_1.opcodes.OP_RETURN != slashingOutput[0]) { + if (slashingAmount <= dustSat_1.BTC_DUST_SAT) { + throw new Error("Slashing amount is less than dust limit"); + } + } + const userFunds = stakingAmount - slashingAmount - minimumFee; + if (userFunds <= dustSat_1.BTC_DUST_SAT) { + throw new Error("User funds are less than dust limit"); + } + const psbt = new bitcoinjs_lib_1.Psbt({ network }); + psbt.setVersion(psbt_1.TRANSACTION_VERSION); + psbt.addInput({ + hash: transaction.getHash(), + index: outputIndex, + tapInternalKey: internalPubkey_1.internalPubkey, + witnessUtxo: { + value: stakingAmount, + script: transaction.outs[outputIndex].script, + }, + tapLeafScript: [tapLeafScript], + // not RBF-able + sequence: psbt_1.NON_RBF_SEQUENCE, + }); + // Add the slashing output + psbt.addOutput({ + script: slashingOutput, + value: slashingAmount, + }); + // Change output contains unbonding timelock script + const changeOutput = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree: { output: scripts.unbondingTimelockScript }, + network, + }); + // Add the change output + psbt.addOutput({ + address: changeOutput.address, + value: userFunds, + }); + // Slashing transaction has no time-based restrictions and can be included + // in the next block immediately. + psbt.setLocktime(0); + return { psbt }; +} +function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) { + // Check that transaction fee is bigger than 0 + if (unbondingFee <= 0) { + throw new Error("Unbonding fee must be bigger than 0"); + } + // Check that outputIndex is bigger or equal to 0 + if (outputIndex < 0) { + throw new Error("Output index must be bigger or equal to 0"); + } + const tx = new bitcoinjs_lib_1.Transaction(); + tx.version = psbt_1.TRANSACTION_VERSION; + tx.addInput(stakingTx.getHash(), outputIndex, psbt_1.NON_RBF_SEQUENCE); + const unbondingOutputInfo = (0, staking_1.deriveUnbondingOutputInfo)(scripts, network); + const outputValue = stakingTx.outs[outputIndex].value - unbondingFee; + if (outputValue < dustSat_1.BTC_DUST_SAT) { + throw new Error("Output value is less than dust limit for unbonding transaction"); + } + // Add the unbonding output + if (!unbondingOutputInfo.outputAddress) { + throw new Error("Unbonding output address is not defined"); + } + tx.addOutput(unbondingOutputInfo.scriptPubKey, outputValue); + // Unbonding transaction has no time-based restrictions and can be included + // in the next block immediately. + tx.locktime = 0; + return { + transaction: tx, + fee: unbondingFee, + }; +} +// This function attaches covenant signatures as the transaction's witness +// Note that the witness script expects exactly covenantQuorum number of signatures +// to match the covenant parameters. +const createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => { + if (covenantSigs.length < covenantQuorum) { + throw new Error(`Not enough covenant signatures. Required: ${covenantQuorum}, ` + + `got: ${covenantSigs.length}`); + } + // Verify all btcPkHex from covenantSigs exist in paramsCovenants + for (const sig of covenantSigs) { + const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex"); + if (!paramsCovenants.some(covenant => covenant.equals(btcPkHexBuf))) { + throw new Error(`Covenant signature public key ${sig.btcPkHex} not found in params covenants`); + } + } + // We only take exactly covenantQuorum number of signatures, even if more are provided. + // Including extra signatures will cause the unbonding transaction to fail validation. + // This is because the witness script expects exactly covenantQuorum number of signatures + // to match the covenant parameters. + const covenantSigsBuffers = covenantSigs + .slice(0, covenantQuorum) + .map((sig) => ({ + btcPkHex: Buffer.from(sig.btcPkHex, "hex"), + sigHex: Buffer.from(sig.sigHex, "hex"), + })); + // we need covenant from params to be sorted in reverse order + const paramsCovenantsSorted = [...paramsCovenants] + .sort(Buffer.compare) + .reverse(); + const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => { + // in case there's covenant with this btc_pk_hex we return the sig + // otherwise we return empty Buffer + const covenantSig = covenantSigsBuffers.find((sig) => sig.btcPkHex.compare(covenant) === 0); + return (covenantSig === null || covenantSig === void 0 ? void 0 : covenantSig.sigHex) || Buffer.alloc(0); + }); + return [...composedCovenantSigs, ...originalWitness]; +}; +exports.createCovenantWitness = createCovenantWitness; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts new file mode 100644 index 0000000000..5e475b085a --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts @@ -0,0 +1,9 @@ +export interface UTXO { + txid: string; + vout: number; + value: number; + scriptPubKey: string; + rawTxHex?: string; + redeemScript?: string; + witnessScript?: string; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts new file mode 100644 index 0000000000..af46404b5b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts @@ -0,0 +1,4 @@ +export interface CovenantSignature { + btcPkHex: string; + sigHex: string; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts new file mode 100644 index 0000000000..92de588970 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts @@ -0,0 +1,3 @@ +export * from "./params"; +export * from "./transaction"; +export * from "./UTXO"; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js new file mode 100644 index 0000000000..0683e30272 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js @@ -0,0 +1,19 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./params"), exports); +__exportStar(require("./transaction"), exports); +__exportStar(require("./UTXO"), exports); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts new file mode 100644 index 0000000000..fad1535517 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts @@ -0,0 +1,34 @@ +/** + * Base interface for staking parameters that define the rules and constraints + * for staking operations. + */ +export interface StakingParams { + covenantNoCoordPks: string[]; + covenantQuorum: number; + unbondingTime: number; + unbondingFeeSat: number; + maxStakingAmountSat: number; + minStakingAmountSat: number; + maxStakingTimeBlocks: number; + minStakingTimeBlocks: number; + slashing?: { + slashingPkScriptHex: string; + slashingRate: number; + minSlashingTxFeeSat: number; + }; +} +/** + * Extension of StakingParams that includes activation height and version information. + * These parameters are used to identify and select the appropriate staking rules at + * different blockchain heights, but do not affect the actual staking transaction content. + */ +export interface VersionedStakingParams extends StakingParams { + btcActivationHeight: number; + version: number; +} +/** + * Extension of VersionedStakingParams that includes a tag field for observability. + */ +export interface ObservableVersionedStakingParams extends VersionedStakingParams { + tag: string; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts new file mode 100644 index 0000000000..c0192b57ed --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts @@ -0,0 +1,16 @@ +import { PsbtOutput } from "bip174/src/lib/interfaces"; +export type PsbtOutputExtended = PsbtOutputExtendedAddress | PsbtOutputExtendedScript; +interface PsbtOutputExtendedAddress extends PsbtOutput { + address: string; + value: number; +} +interface PsbtOutputExtendedScript extends PsbtOutput { + script: Buffer; + value: number; +} +export declare const isPsbtOutputExtendedAddress: (output: PsbtOutputExtended) => output is PsbtOutputExtendedAddress; +export type TransactionOutput = { + scriptPubKey: Buffer; + value: number; +}; +export {}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js new file mode 100644 index 0000000000..d09308781e --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isPsbtOutputExtendedAddress = void 0; +const isPsbtOutputExtendedAddress = (output) => { + return output.address !== undefined; +}; +exports.isPsbtOutputExtendedAddress = isPsbtOutputExtendedAddress; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts new file mode 100644 index 0000000000..5298388ae6 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts @@ -0,0 +1,15 @@ +import { Psbt, Transaction } from "bitcoinjs-lib"; +/** + * PsbtResult is an object containing a partially signed transaction and its fee + */ +export interface PsbtResult { + psbt: Psbt; + fee: number; +} +/** + * TransactionResult is an object containing an unsigned transaction and its fee + */ +export interface TransactionResult { + transaction: Transaction; + fee: number; +} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts new file mode 100644 index 0000000000..047058574f --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts @@ -0,0 +1,7 @@ +/** + * Validates a Babylon address. Babylon addresses are encoded in Bech32 format + * and have a prefix of "bbn". + * @param address - The address to validate. + * @returns True if the address is valid, false otherwise. + */ +export declare const isValidBabylonAddress: (address: string) => boolean; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js new file mode 100644 index 0000000000..0d301d156d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.isValidBabylonAddress = void 0; +const encoding_1 = require("@cosmjs/encoding"); +/** + * Validates a Babylon address. Babylon addresses are encoded in Bech32 format + * and have a prefix of "bbn". + * @param address - The address to validate. + * @returns True if the address is valid, false otherwise. + */ +const isValidBabylonAddress = (address) => { + try { + const { prefix } = (0, encoding_1.fromBech32)(address); + return prefix === "bbn"; + } + catch (error) { + return false; + } +}; +exports.isValidBabylonAddress = isValidBabylonAddress; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts new file mode 100644 index 0000000000..c24f33a3a3 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts @@ -0,0 +1,48 @@ +import { networks } from "bitcoinjs-lib"; +export declare const initBTCCurve: () => void; +/** + * Check whether the given address is a valid Bitcoin address. + * + * @param {string} btcAddress - The Bitcoin address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is valid, otherwise false. + */ +export declare const isValidBitcoinAddress: (btcAddress: string, network: networks.Network) => boolean; +/** + * Check whether the given address is a Taproot address. + * + * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is a Taproot address, otherwise false. + */ +export declare const isTaproot: (taprootAddress: string, network: networks.Network) => boolean; +/** + * Check whether the given address is a Native SegWit address. + * + * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is a Native SegWit address, otherwise false. + */ +export declare const isNativeSegwit: (segwitAddress: string, network: networks.Network) => boolean; +/** + * Check whether the given public key is a valid public key without a coordinate. + * + * @param {string} pkWithNoCoord - public key without the coordinate. + * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false. + */ +export declare const isValidNoCoordPublicKey: (pkWithNoCoord: string) => boolean; +/** + * Get the public key without the coordinate. + * + * @param {string} pkHex - The public key in hex, with or without the coordinate. + * @returns {string} - The public key without the coordinate in hex. + * @throws {Error} - If the public key is invalid. + */ +export declare const getPublicKeyNoCoord: (pkHex: string) => String; +/** + * Convert a transaction id to a hash. in buffer format. + * + * @param {string} txId - The transaction id. + * @returns {Buffer} - The transaction hash. + */ +export declare const transactionIdToHash: (txId: string) => Buffer; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js new file mode 100644 index 0000000000..4402ee8be1 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js @@ -0,0 +1,179 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.transactionIdToHash = exports.getPublicKeyNoCoord = exports.isValidNoCoordPublicKey = exports.isNativeSegwit = exports.isTaproot = exports.isValidBitcoinAddress = exports.initBTCCurve = void 0; +const ecc = __importStar(require("@bitcoin-js/tiny-secp256k1-asmjs")); +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const keys_1 = require("../constants/keys"); +// Initialize elliptic curve library +const initBTCCurve = () => { + (0, bitcoinjs_lib_1.initEccLib)(ecc); +}; +exports.initBTCCurve = initBTCCurve; +/** + * Check whether the given address is a valid Bitcoin address. + * + * @param {string} btcAddress - The Bitcoin address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is valid, otherwise false. + */ +const isValidBitcoinAddress = (btcAddress, network) => { + try { + return !!bitcoinjs_lib_1.address.toOutputScript(btcAddress, network); + } + catch (error) { + return false; + } +}; +exports.isValidBitcoinAddress = isValidBitcoinAddress; +/** + * Check whether the given address is a Taproot address. + * + * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is a Taproot address, otherwise false. + */ +const isTaproot = (taprootAddress, network) => { + try { + const decoded = bitcoinjs_lib_1.address.fromBech32(taprootAddress); + if (decoded.version !== 1) { + return false; + } + // Compare network properties instead of object reference + // The bech32 is hardcoded in the bitcoinjs-lib library. + // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36 + if (network.bech32 === bitcoinjs_lib_1.networks.bitcoin.bech32) { + // Check if address starts with "bc1p" + return taprootAddress.startsWith("bc1p"); + } + else if (network.bech32 === bitcoinjs_lib_1.networks.testnet.bech32) { + // signet, regtest and testnet taproot addresses start with "tb1p" or "sb1p" + return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p"); + } + return false; + } + catch (error) { + return false; + } +}; +exports.isTaproot = isTaproot; +/** + * Check whether the given address is a Native SegWit address. + * + * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check. + * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). + * @returns {boolean} - True if the address is a Native SegWit address, otherwise false. + */ +const isNativeSegwit = (segwitAddress, network) => { + try { + const decoded = bitcoinjs_lib_1.address.fromBech32(segwitAddress); + if (decoded.version !== 0) { + return false; + } + // Compare network properties instead of object reference + // The bech32 is hardcoded in the bitcoinjs-lib library. + // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36 + if (network.bech32 === bitcoinjs_lib_1.networks.bitcoin.bech32) { + // Check if address starts with "bc1q" + return segwitAddress.startsWith("bc1q"); + } + else if (network.bech32 === bitcoinjs_lib_1.networks.testnet.bech32) { + // testnet native segwit addresses start with "tb1q" + return segwitAddress.startsWith("tb1q"); + } + return false; + } + catch (error) { + return false; + } +}; +exports.isNativeSegwit = isNativeSegwit; +/** + * Check whether the given public key is a valid public key without a coordinate. + * + * @param {string} pkWithNoCoord - public key without the coordinate. + * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false. + */ +const isValidNoCoordPublicKey = (pkWithNoCoord) => { + try { + const keyBuffer = Buffer.from(pkWithNoCoord, 'hex'); + return validateNoCoordPublicKeyBuffer(keyBuffer); + } + catch (error) { + return false; + } +}; +exports.isValidNoCoordPublicKey = isValidNoCoordPublicKey; +/** + * Get the public key without the coordinate. + * + * @param {string} pkHex - The public key in hex, with or without the coordinate. + * @returns {string} - The public key without the coordinate in hex. + * @throws {Error} - If the public key is invalid. + */ +const getPublicKeyNoCoord = (pkHex) => { + const publicKey = Buffer.from(pkHex, "hex"); + const publicKeyNoCoordBuffer = publicKey.length === keys_1.NO_COORD_PK_BYTE_LENGTH + ? publicKey + : publicKey.subarray(1, 33); + // Validate the public key without coordinate + if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) { + throw new Error("Invalid public key without coordinate"); + } + return publicKeyNoCoordBuffer.toString("hex"); +}; +exports.getPublicKeyNoCoord = getPublicKeyNoCoord; +const validateNoCoordPublicKeyBuffer = (pkBuffer) => { + if (pkBuffer.length !== keys_1.NO_COORD_PK_BYTE_LENGTH) { + return false; + } + // Try both compressed forms: y-coordinate even (0x02) and y-coordinate odd (0x03) + const compressedKeyEven = Buffer.concat([Buffer.from([0x02]), pkBuffer]); + const compressedKeyOdd = Buffer.concat([Buffer.from([0x03]), pkBuffer]); + return (ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd)); +}; +/** + * Convert a transaction id to a hash. in buffer format. + * + * @param {string} txId - The transaction id. + * @returns {Buffer} - The transaction hash. + */ +const transactionIdToHash = (txId) => { + if (txId === "") { + throw new Error("Transaction id cannot be empty"); + } + return Buffer.from(txId, 'hex').reverse(); +}; +exports.transactionIdToHash = transactionIdToHash; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts new file mode 100644 index 0000000000..8d48f76130 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts @@ -0,0 +1,33 @@ +import { UTXO } from "../../types/UTXO"; +import { TransactionOutput } from "../../types/psbtOutputs"; +/** + * Selects UTXOs and calculates the fee for a staking transaction. + * This method selects the highest value UTXOs from all available UTXOs to + * cover the staking amount and the transaction fees. + * The formula used is: + * + * totalFee = (inputSize + outputSize) * feeRate + buffer + * where outputSize may or may not include the change output size depending on the remaining value. + * + * @param availableUTXOs - All available UTXOs from the wallet. + * @param stakingAmount - The amount to stake. + * @param feeRate - The fee rate in satoshis per byte. + * @param outputs - The outputs in the transaction. + * @returns An object containing the selected UTXOs and the fee. + * @throws Will throw an error if there are insufficient funds or if the fee cannot be calculated. + */ +export declare const getStakingTxInputUTXOsAndFees: (availableUTXOs: UTXO[], stakingAmount: number, feeRate: number, outputs: TransactionOutput[]) => { + selectedUTXOs: UTXO[]; + fee: number; +}; +/** + * Calculates the estimated fee for a withdrawal transaction. + * The fee calculation is based on estimated constants for input size, + * output size, and additional overhead specific to withdrawal transactions. + * Due to the slightly larger size of withdrawal transactions, an additional + * buffer is included to account for this difference. + * + * @param feeRate - The fee rate in satoshis per vbyte. + * @returns The estimated fee for a withdrawal transaction in satoshis. + */ +export declare const getWithdrawTxFee: (feeRate: number) => number; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js new file mode 100644 index 0000000000..a874ab5527 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js @@ -0,0 +1,137 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getWithdrawTxFee = exports.getStakingTxInputUTXOsAndFees = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const dustSat_1 = require("../../constants/dustSat"); +const fee_1 = require("../../constants/fee"); +const utils_1 = require("./utils"); +/** + * Selects UTXOs and calculates the fee for a staking transaction. + * This method selects the highest value UTXOs from all available UTXOs to + * cover the staking amount and the transaction fees. + * The formula used is: + * + * totalFee = (inputSize + outputSize) * feeRate + buffer + * where outputSize may or may not include the change output size depending on the remaining value. + * + * @param availableUTXOs - All available UTXOs from the wallet. + * @param stakingAmount - The amount to stake. + * @param feeRate - The fee rate in satoshis per byte. + * @param outputs - The outputs in the transaction. + * @returns An object containing the selected UTXOs and the fee. + * @throws Will throw an error if there are insufficient funds or if the fee cannot be calculated. + */ +const getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => { + if (availableUTXOs.length === 0) { + throw new Error("Insufficient funds"); + } + const validUTXOs = availableUTXOs.filter((utxo) => { + const script = Buffer.from(utxo.scriptPubKey, "hex"); + return !!bitcoinjs_lib_1.script.decompile(script); + }); + if (validUTXOs.length === 0) { + throw new Error("Insufficient funds: no valid UTXOs available for staking"); + } + // Sort available UTXOs from highest to lowest value + const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value); + const selectedUTXOs = []; + let accumulatedValue = 0; + let estimatedFee = 0; + for (const utxo of sortedUTXOs) { + selectedUTXOs.push(utxo); + accumulatedValue += utxo.value; + // Calculate the fee for the current set of UTXOs and outputs + const estimatedSize = getEstimatedSize(selectedUTXOs, outputs); + estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate); + // Check if there will be any change left after the staking amount and fee. + // If there is, a change output needs to be added, which also comes with an additional fee. + if (accumulatedValue - (stakingAmount + estimatedFee) > dustSat_1.BTC_DUST_SAT) { + estimatedFee += (0, utils_1.getEstimatedChangeOutputSize)() * feeRate; + } + if (accumulatedValue >= stakingAmount + estimatedFee) { + break; + } + } + if (accumulatedValue < stakingAmount + estimatedFee) { + throw new Error("Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"); + } + return { + selectedUTXOs, + fee: estimatedFee, + }; +}; +exports.getStakingTxInputUTXOsAndFees = getStakingTxInputUTXOsAndFees; +/** + * Calculates the estimated fee for a withdrawal transaction. + * The fee calculation is based on estimated constants for input size, + * output size, and additional overhead specific to withdrawal transactions. + * Due to the slightly larger size of withdrawal transactions, an additional + * buffer is included to account for this difference. + * + * @param feeRate - The fee rate in satoshis per vbyte. + * @returns The estimated fee for a withdrawal transaction in satoshis. + */ +const getWithdrawTxFee = (feeRate) => { + const inputSize = fee_1.P2TR_INPUT_SIZE; + const outputSize = (0, utils_1.getEstimatedChangeOutputSize)(); + return (feeRate * + (inputSize + + outputSize + + fee_1.TX_BUFFER_SIZE_OVERHEAD + + fee_1.WITHDRAW_TX_BUFFER_SIZE) + + rateBasedTxBufferFee(feeRate)); +}; +exports.getWithdrawTxFee = getWithdrawTxFee; +/** + * Calculates the estimated transaction size using a heuristic formula which + * includes the input size, output size, and a fixexd buffer for the transaction size. + * The formula used is: + * + * totalSize = inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD + * + * @param inputUtxos - The UTXOs used as inputs in the transaction. + * @param outputs - The outputs in the transaction. + * @returns The estimated transaction size in bytes. + */ +const getEstimatedSize = (inputUtxos, outputs) => { + // Estimate the input size + const inputSize = inputUtxos.reduce((acc, u) => { + const script = Buffer.from(u.scriptPubKey, "hex"); + const decompiledScript = bitcoinjs_lib_1.script.decompile(script); + if (!decompiledScript) { + // Skip UTXOs with scripts that cannot be decompiled + return acc; + } + return acc + (0, utils_1.getInputSizeByScript)(script); + }, 0); + // Estimate the output size + const outputSize = outputs.reduce((acc, output) => { + if ((0, utils_1.isOP_RETURN)(output.scriptPubKey)) { + return (acc + + output.scriptPubKey.length + + fee_1.OP_RETURN_OUTPUT_VALUE_SIZE + + fee_1.OP_RETURN_VALUE_SERIALIZE_SIZE); + } + return acc + fee_1.MAX_NON_LEGACY_OUTPUT_SIZE; + }, 0); + return inputSize + outputSize + fee_1.TX_BUFFER_SIZE_OVERHEAD; +}; +/** + * Adds a buffer to the transaction size-based fee calculation if the fee rate is low. + * Some wallets have a relayer fee requirement, which means if the fee rate is + * less than or equal to WALLET_RELAY_FEE_RATE_THRESHOLD (2 satoshis per byte), + * there is a risk that the fee might not be sufficient to get the transaction relayed. + * To mitigate this risk, we add a buffer to the fee calculation to ensure that + * the transaction can be relayed. + * + * If the fee rate is less than or equal to WALLET_RELAY_FEE_RATE_THRESHOLD, a fixed buffer is added + * (LOW_RATE_ESTIMATION_ACCURACY_BUFFER). If the fee rate is higher, no buffer is added. + * + * @param feeRate - The fee rate in satoshis per byte. + * @returns The buffer amount in satoshis to be added to the transaction fee. + */ +const rateBasedTxBufferFee = (feeRate) => { + return feeRate <= fee_1.WALLET_RELAY_FEE_RATE_THRESHOLD + ? fee_1.LOW_RATE_ESTIMATION_ACCURACY_BUFFER + : 0; +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts new file mode 100644 index 0000000000..116901cc82 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts @@ -0,0 +1,23 @@ +import { UTXO } from "../../types/UTXO"; +export declare const isOP_RETURN: (script: Buffer) => boolean; +/** + * Determines the size of a transaction input based on its script type. + * + * @param script - The script of the input. + * @returns The estimated size of the input in bytes. + */ +export declare const getInputSizeByScript: (script: Buffer) => number; +/** + * Returns the estimated size for a change output. + * This is used when the transaction has a change output to a particular address. + * + * @returns The estimated size for a change output in bytes. + */ +export declare const getEstimatedChangeOutputSize: () => number; +/** + * Returns the sum of the values of the UTXOs. + * + * @param inputUTXOs - The UTXOs to sum the values of. + * @returns The sum of the values of the UTXOs in satoshis. + */ +export declare const inputValueSum: (inputUTXOs: UTXO[]) => number; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js new file mode 100644 index 0000000000..087a4b916d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js @@ -0,0 +1,66 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.inputValueSum = exports.getEstimatedChangeOutputSize = exports.getInputSizeByScript = exports.isOP_RETURN = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const fee_1 = require("../../constants/fee"); +// Helper function to check if a script is OP_RETURN +const isOP_RETURN = (script) => { + const decompiled = bitcoinjs_lib_1.script.decompile(script); + return !!decompiled && decompiled[0] === bitcoinjs_lib_1.opcodes.OP_RETURN; +}; +exports.isOP_RETURN = isOP_RETURN; +/** + * Determines the size of a transaction input based on its script type. + * + * @param script - The script of the input. + * @returns The estimated size of the input in bytes. + */ +const getInputSizeByScript = (script) => { + // Check if input is in the format of "00 <20-byte public key hash>" + // If yes, it is a P2WPKH input + try { + const { address: p2wpkhAddress } = bitcoinjs_lib_1.payments.p2wpkh({ + output: script, + }); + if (p2wpkhAddress) { + return fee_1.P2WPKH_INPUT_SIZE; + } + // eslint-disable-next-line no-empty + } + catch (error) { } // Ignore errors + // Check if input is in the format of "51 <32-byte public key>" + // If yes, it is a P2TR input + try { + const { address: p2trAddress } = bitcoinjs_lib_1.payments.p2tr({ + output: script, + }); + if (p2trAddress) { + return fee_1.P2TR_INPUT_SIZE; + } + // eslint-disable-next-line no-empty + } + catch (error) { } // Ignore errors + // Otherwise, assume the input is largest P2PKH address type + return fee_1.DEFAULT_INPUT_SIZE; +}; +exports.getInputSizeByScript = getInputSizeByScript; +/** + * Returns the estimated size for a change output. + * This is used when the transaction has a change output to a particular address. + * + * @returns The estimated size for a change output in bytes. + */ +const getEstimatedChangeOutputSize = () => { + return fee_1.MAX_NON_LEGACY_OUTPUT_SIZE; +}; +exports.getEstimatedChangeOutputSize = getEstimatedChangeOutputSize; +/** + * Returns the sum of the values of the UTXOs. + * + * @param inputUTXOs - The UTXOs to sum the values of. + * @returns The sum of the values of the UTXOs in satoshis. + */ +const inputValueSum = (inputUTXOs) => { + return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0); +}; +exports.inputValueSum = inputValueSum; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts new file mode 100644 index 0000000000..aeea8f0017 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts @@ -0,0 +1,12 @@ +/** + * Reverses the order of bytes in a buffer. + * @param buffer - The buffer to reverse. + * @returns A new buffer with the bytes reversed. + */ +export declare const reverseBuffer: (buffer: Uint8Array) => Uint8Array; +/** + * Converts a Uint8Array to a hexadecimal string. + * @param uint8Array - The Uint8Array to convert. + * @returns The hexadecimal string. + */ +export declare const uint8ArrayToHex: (uint8Array: Uint8Array) => string; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js new file mode 100644 index 0000000000..883142beeb --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.uint8ArrayToHex = exports.reverseBuffer = void 0; +/** + * Reverses the order of bytes in a buffer. + * @param buffer - The buffer to reverse. + * @returns A new buffer with the bytes reversed. + */ +const reverseBuffer = (buffer) => { + const clonedBuffer = new Uint8Array(buffer); + if (clonedBuffer.length < 1) + return clonedBuffer; + for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) { + let tmp = clonedBuffer[i]; + clonedBuffer[i] = clonedBuffer[j]; + clonedBuffer[j] = tmp; + } + return clonedBuffer; +}; +exports.reverseBuffer = reverseBuffer; +/** + * Converts a Uint8Array to a hexadecimal string. + * @param uint8Array - The Uint8Array to convert. + * @returns The hexadecimal string. + */ +const uint8ArrayToHex = (uint8Array) => { + return Array.from(uint8Array) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +}; +exports.uint8ArrayToHex = uint8ArrayToHex; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts new file mode 100644 index 0000000000..375e67b627 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts @@ -0,0 +1,116 @@ +import { networks, Transaction } from "bitcoinjs-lib"; +import { TransactionOutput } from "../../types/psbtOutputs"; +import { UTXO } from "../../types/UTXO"; +import { StakingParams } from "../../types/params"; +export interface OutputInfo { + scriptPubKey: Buffer; + outputAddress: string; +} +/** + * Build the staking output for the transaction which contains p2tr output + * with staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} amount - The amount to stake. + * @returns {TransactionOutput[]} - The staking transaction outputs. + * @throws {Error} - If the staking output cannot be built. + */ +export declare const buildStakingTransactionOutputs: (scripts: { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; + dataEmbedScript?: Buffer; +}, network: networks.Network, amount: number) => TransactionOutput[]; +/** + * Derive the staking output address from the staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @returns {StakingOutput} - The staking output address and scriptPubKey. + * @throws {StakingError} - If the staking output address cannot be derived. + */ +export declare const deriveStakingOutputInfo: (scripts: { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; +}, network: networks.Network) => { + outputAddress: string; + scriptPubKey: Buffer; +}; +/** + * Derive the unbonding output address and scriptPubKey from the staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @returns {OutputInfo} - The unbonding output address and scriptPubKey. + * @throws {StakingError} - If the unbonding output address cannot be derived. + */ +export declare const deriveUnbondingOutputInfo: (scripts: { + unbondingTimelockScript: Buffer; + slashingScript: Buffer; +}, network: networks.Network) => { + outputAddress: string; + scriptPubKey: Buffer; +}; +/** + * Derive the slashing output address and scriptPubKey from the staking scripts. + * + * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the + * unbonding timelock script as the timelock of the slashing transaction. + * This is due to slashing tx timelock is the same as the unbonding timelock. + * @param {networks.Network} network - The Bitcoin network. + * @returns {OutputInfo} - The slashing output address and scriptPubKey. + * @throws {StakingError} - If the slashing output address cannot be derived. + */ +export declare const deriveSlashingOutput: (scripts: { + unbondingTimelockScript: Buffer; +}, network: networks.Network) => { + outputAddress: string; + scriptPubKey: Buffer; +}; +/** + * Find the matching output index for the given transaction. + * + * @param {Transaction} tx - The transaction. + * @param {string} outputAddress - The output address. + * @param {networks.Network} network - The Bitcoin network. + * @returns {number} - The output index. + * @throws {Error} - If the matching output is not found. + */ +export declare const findMatchingTxOutputIndex: (tx: Transaction, outputAddress: string, network: networks.Network) => number; +/** + * Validate the staking transaction input data. + * + * @param {number} stakingAmountSat - The staking amount in satoshis. + * @param {number} timelock - The staking time in blocks. + * @param {StakingParams} params - The staking parameters. + * @param {UTXO[]} inputUTXOs - The input UTXOs. + * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte + * @throws {StakingError} - If the input data is invalid. + */ +export declare const validateStakingTxInputData: (stakingAmountSat: number, timelock: number, params: StakingParams, inputUTXOs: UTXO[], feeRate: number) => void; +/** + * Validate the staking parameters. + * Extend this method to add additional validation for staking parameters based + * on the staking type. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the parameters are invalid. + */ +export declare const validateParams: (params: StakingParams) => void; +/** + * Validate the staking timelock. + * + * @param {number} stakingTimelock - The staking timelock. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the staking timelock is invalid. + */ +export declare const validateStakingTimelock: (stakingTimelock: number, params: StakingParams) => void; +/** + * toBuffers converts an array of strings to an array of buffers. + * + * @param {string[]} inputs - The input strings. + * @returns {Buffer[]} - The buffers. + * @throws {StakingError} - If the values cannot be converted to buffers. + */ +export declare const toBuffers: (inputs: string[]) => Buffer[]; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js new file mode 100644 index 0000000000..5c2b68e2a7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js @@ -0,0 +1,256 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.toBuffers = exports.validateStakingTimelock = exports.validateParams = exports.validateStakingTxInputData = exports.findMatchingTxOutputIndex = exports.deriveSlashingOutput = exports.deriveUnbondingOutputInfo = exports.deriveStakingOutputInfo = exports.buildStakingTransactionOutputs = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +const internalPubkey_1 = require("../../constants/internalPubkey"); +const error_1 = require("../../error"); +const btc_1 = require("../btc"); +const unbonding_1 = require("../../constants/unbonding"); +/** + * Build the staking output for the transaction which contains p2tr output + * with staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @param {number} amount - The amount to stake. + * @returns {TransactionOutput[]} - The staking transaction outputs. + * @throws {Error} - If the staking output cannot be built. + */ +const buildStakingTransactionOutputs = (scripts, network, amount) => { + const stakingOutputInfo = (0, exports.deriveStakingOutputInfo)(scripts, network); + const transactionOutputs = [ + { + scriptPubKey: stakingOutputInfo.scriptPubKey, + value: amount, + }, + ]; + if (scripts.dataEmbedScript) { + // Add the data embed output to the transaction + transactionOutputs.push({ + scriptPubKey: scripts.dataEmbedScript, + value: 0, + }); + } + return transactionOutputs; +}; +exports.buildStakingTransactionOutputs = buildStakingTransactionOutputs; +/** + * Derive the staking output address from the staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @returns {StakingOutput} - The staking output address and scriptPubKey. + * @throws {StakingError} - If the staking output address cannot be derived. + */ +const deriveStakingOutputInfo = (scripts, network) => { + // Build outputs + const scriptTree = [ + { + output: scripts.slashingScript, + }, + [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], + ]; + // Create an pay-2-taproot (p2tr) output using the staking script + const stakingOutput = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree, + network, + }); + if (!stakingOutput.address) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build staking output"); + } + return { + outputAddress: stakingOutput.address, + scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(stakingOutput.address, network), + }; +}; +exports.deriveStakingOutputInfo = deriveStakingOutputInfo; +/** + * Derive the unbonding output address and scriptPubKey from the staking scripts. + * + * @param {StakingScripts} scripts - The staking scripts. + * @param {networks.Network} network - The Bitcoin network. + * @returns {OutputInfo} - The unbonding output address and scriptPubKey. + * @throws {StakingError} - If the unbonding output address cannot be derived. + */ +const deriveUnbondingOutputInfo = (scripts, network) => { + const outputScriptTree = [ + { + output: scripts.slashingScript, + }, + { output: scripts.unbondingTimelockScript }, + ]; + const unbondingOutput = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree: outputScriptTree, + network, + }); + if (!unbondingOutput.address) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build unbonding output"); + } + return { + outputAddress: unbondingOutput.address, + scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(unbondingOutput.address, network), + }; +}; +exports.deriveUnbondingOutputInfo = deriveUnbondingOutputInfo; +/** + * Derive the slashing output address and scriptPubKey from the staking scripts. + * + * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the + * unbonding timelock script as the timelock of the slashing transaction. + * This is due to slashing tx timelock is the same as the unbonding timelock. + * @param {networks.Network} network - The Bitcoin network. + * @returns {OutputInfo} - The slashing output address and scriptPubKey. + * @throws {StakingError} - If the slashing output address cannot be derived. + */ +const deriveSlashingOutput = (scripts, network) => { + const slashingOutput = bitcoinjs_lib_1.payments.p2tr({ + internalPubkey: internalPubkey_1.internalPubkey, + scriptTree: { output: scripts.unbondingTimelockScript }, + network, + }); + const slashingOutputAddress = slashingOutput.address; + if (!slashingOutputAddress) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build slashing output address"); + } + return { + outputAddress: slashingOutputAddress, + scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(slashingOutputAddress, network), + }; +}; +exports.deriveSlashingOutput = deriveSlashingOutput; +/** + * Find the matching output index for the given transaction. + * + * @param {Transaction} tx - The transaction. + * @param {string} outputAddress - The output address. + * @param {networks.Network} network - The Bitcoin network. + * @returns {number} - The output index. + * @throws {Error} - If the matching output is not found. + */ +const findMatchingTxOutputIndex = (tx, outputAddress, network) => { + const index = tx.outs.findIndex(output => { + return bitcoinjs_lib_1.address.fromOutputScript(output.script, network) === outputAddress; + }); + if (index === -1) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, `Matching output not found for address: ${outputAddress}`); + } + return index; +}; +exports.findMatchingTxOutputIndex = findMatchingTxOutputIndex; +/** + * Validate the staking transaction input data. + * + * @param {number} stakingAmountSat - The staking amount in satoshis. + * @param {number} timelock - The staking time in blocks. + * @param {StakingParams} params - The staking parameters. + * @param {UTXO[]} inputUTXOs - The input UTXOs. + * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte + * @throws {StakingError} - If the input data is invalid. + */ +const validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => { + if (stakingAmountSat < params.minStakingAmountSat || + stakingAmountSat > params.maxStakingAmountSat) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staking amount"); + } + if (timelock < params.minStakingTimeBlocks || + timelock > params.maxStakingTimeBlocks) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid timelock"); + } + if (inputUTXOs.length == 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "No input UTXOs provided"); + } + if (feeRate <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid fee rate"); + } +}; +exports.validateStakingTxInputData = validateStakingTxInputData; +/** + * Validate the staking parameters. + * Extend this method to add additional validation for staking parameters based + * on the staking type. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the parameters are invalid. + */ +const validateParams = (params) => { + // Check covenant public keys + if (params.covenantNoCoordPks.length == 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Could not find any covenant public keys"); + } + if (params.covenantNoCoordPks.length < params.covenantQuorum) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant public keys must be greater than or equal to the quorum"); + } + params.covenantNoCoordPks.forEach((pk) => { + if (!(0, btc_1.isValidNoCoordPublicKey)(pk)) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant public key should contains no coordinate"); + } + }); + // Check other parameters + if (params.unbondingTime <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Unbonding time must be greater than 0"); + } + if (params.unbondingFeeSat <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Unbonding fee must be greater than 0"); + } + if (params.maxStakingAmountSat < params.minStakingAmountSat) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Max staking amount must be greater or equal to min staking amount"); + } + if (params.minStakingAmountSat < params.unbondingFeeSat + unbonding_1.MIN_UNBONDING_OUTPUT_VALUE) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, `Min staking amount must be greater than unbonding fee plus ${unbonding_1.MIN_UNBONDING_OUTPUT_VALUE}`); + } + if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Max staking time must be greater or equal to min staking time"); + } + if (params.minStakingTimeBlocks <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Min staking time must be greater than 0"); + } + if (params.covenantQuorum <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant quorum must be greater than 0"); + } + if (params.slashing) { + if (params.slashing.slashingRate <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing rate must be greater than 0"); + } + if (params.slashing.slashingRate > 1) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing rate must be less or equal to 1"); + } + if (params.slashing.slashingPkScriptHex.length == 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing public key script is missing"); + } + if (params.slashing.minSlashingTxFeeSat <= 0) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Minimum slashing transaction fee must be greater than 0"); + } + } +}; +exports.validateParams = validateParams; +/** + * Validate the staking timelock. + * + * @param {number} stakingTimelock - The staking timelock. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the staking timelock is invalid. + */ +const validateStakingTimelock = (stakingTimelock, params) => { + if (stakingTimelock < params.minStakingTimeBlocks || + stakingTimelock > params.maxStakingTimeBlocks) { + throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Staking transaction timelock is out of range"); + } +}; +exports.validateStakingTimelock = validateStakingTimelock; +/** + * toBuffers converts an array of strings to an array of buffers. + * + * @param {string[]} inputs - The input strings. + * @returns {Buffer[]} - The buffers. + * @throws {StakingError} - If the values cannot be converted to buffers. + */ +const toBuffers = (inputs) => { + try { + return inputs.map((i) => Buffer.from(i, "hex")); + } + catch (error) { + throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.INVALID_INPUT, "Cannot convert values to buffers"); + } +}; +exports.toBuffers = toBuffers; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts new file mode 100644 index 0000000000..519466725d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts @@ -0,0 +1,3 @@ +import { StakingParams, VersionedStakingParams } from "../../types/params"; +export declare const getBabylonParamByBtcHeight: (height: number, babylonParamsVersions: VersionedStakingParams[]) => StakingParams; +export declare const getBabylonParamByVersion: (version: number, babylonParams: VersionedStakingParams[]) => StakingParams; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js new file mode 100644 index 0000000000..1e8bff47cc --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js @@ -0,0 +1,32 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getBabylonParamByVersion = exports.getBabylonParamByBtcHeight = void 0; +/* + Get the Babylon params version by BTC height + @param height - The BTC height + @param babylonParamsVersions - The Babylon params versions + @returns The Babylon params +*/ +const getBabylonParamByBtcHeight = (height, babylonParamsVersions) => { + // Sort by btcActivationHeight in ascending order + const sortedParams = [...babylonParamsVersions].sort((a, b) => b.btcActivationHeight - a.btcActivationHeight); + // Find first params where height is >= btcActivationHeight + const params = sortedParams.find((p) => height >= p.btcActivationHeight); + if (!params) + throw new Error(`Babylon params not found for height ${height}`); + return params; +}; +exports.getBabylonParamByBtcHeight = getBabylonParamByBtcHeight; +/* + Get the Babylon params by version + @param version - The Babylon params version + @param babylonParams - The Babylon params + @returns The Babylon params +*/ +const getBabylonParamByVersion = (version, babylonParams) => { + const params = babylonParams.find((p) => p.version === version); + if (!params) + throw new Error(`Babylon params not found for version ${version}`); + return params; +}; +exports.getBabylonParamByVersion = getBabylonParamByVersion; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts new file mode 100644 index 0000000000..de084faedf --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts @@ -0,0 +1,3 @@ +import { Input } from "bitcoinjs-lib/src/transaction"; +import { UTXO } from "../../types/UTXO"; +export declare const findInputUTXO: (inputUTXOs: UTXO[], input: Input) => UTXO; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js new file mode 100644 index 0000000000..2b1057c246 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.findInputUTXO = void 0; +const btc_1 = require("../btc"); +const findInputUTXO = (inputUTXOs, input) => { + const inputUTXO = inputUTXOs.find((u) => (0, btc_1.transactionIdToHash)(u.txid).toString("hex") === + input.hash.toString("hex") && u.vout === input.index); + if (!inputUTXO) { + throw new Error(`Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} ` + + `and vout: ${input.index}`); + } + return inputUTXO; +}; +exports.findInputUTXO = findInputUTXO; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts new file mode 100644 index 0000000000..555848ace7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts @@ -0,0 +1,13 @@ +import { PsbtInputExtended } from "bip174/src/lib/interfaces"; +import { UTXO } from "../../types"; +/** + * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type. + * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns + * the appropriate PSBT input fields required for that UTXO. + * + * @param {UTXO} utxo - The unspent transaction output to process + * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional). + * @returns {object} PSBT input fields object containing the necessary data + * @throws {Error} If required input data is missing or if an unsupported script type is provided + */ +export declare const getPsbtInputFields: (utxo: UTXO, publicKeyNoCoord?: Buffer) => PsbtInputExtended; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js new file mode 100644 index 0000000000..c68baee77d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getPsbtInputFields = void 0; +const getScriptType_1 = require("./getScriptType"); +/** + * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type. + * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns + * the appropriate PSBT input fields required for that UTXO. + * + * @param {UTXO} utxo - The unspent transaction output to process + * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional). + * @returns {object} PSBT input fields object containing the necessary data + * @throws {Error} If required input data is missing or if an unsupported script type is provided + */ +const getPsbtInputFields = (utxo, publicKeyNoCoord) => { + const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex"); + const type = (0, getScriptType_1.getScriptType)(scriptPubKey); + switch (type) { + case getScriptType_1.BitcoinScriptType.P2PKH: { + if (!utxo.rawTxHex) { + throw new Error("Missing rawTxHex for legacy P2PKH input"); + } + return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") }; + } + case getScriptType_1.BitcoinScriptType.P2SH: { + if (!utxo.rawTxHex) { + throw new Error("Missing rawTxHex for P2SH input"); + } + if (!utxo.redeemScript) { + throw new Error("Missing redeemScript for P2SH input"); + } + return { + nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"), + redeemScript: Buffer.from(utxo.redeemScript, "hex"), + }; + } + case getScriptType_1.BitcoinScriptType.P2WPKH: { + return { + witnessUtxo: { + script: scriptPubKey, + value: utxo.value, + }, + }; + } + case getScriptType_1.BitcoinScriptType.P2WSH: { + if (!utxo.witnessScript) { + throw new Error("Missing witnessScript for P2WSH input"); + } + return { + witnessUtxo: { + script: scriptPubKey, + value: utxo.value, + }, + witnessScript: Buffer.from(utxo.witnessScript, "hex"), + }; + } + case getScriptType_1.BitcoinScriptType.P2TR: { + return Object.assign({ witnessUtxo: { + script: scriptPubKey, + value: utxo.value, + } }, (publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord })); + } + default: + throw new Error(`Unsupported script type: ${type}`); + } +}; +exports.getPsbtInputFields = getPsbtInputFields; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts new file mode 100644 index 0000000000..b5f970e920 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts @@ -0,0 +1,21 @@ +/** + * Supported Bitcoin script types + */ +export declare enum BitcoinScriptType { + P2PKH = "pubkeyhash", + P2SH = "scripthash", + P2WPKH = "witnesspubkeyhash", + P2WSH = "witnessscripthash", + P2TR = "taproot" +} +/** + * Determines the type of Bitcoin script. + * + * This function tries to parse the script using different Bitcoin payment types and returns + * a string identifier for the script type. + * + * @param script - The raw script as a Buffer + * @returns {BitcoinScriptType} The identified script type + * @throws {Error} If the script cannot be identified as any known type + */ +export declare const getScriptType: (script: Buffer) => BitcoinScriptType; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js new file mode 100644 index 0000000000..39934f5677 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js @@ -0,0 +1,59 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getScriptType = exports.BitcoinScriptType = void 0; +const bitcoinjs_lib_1 = require("bitcoinjs-lib"); +/** + * Supported Bitcoin script types + */ +var BitcoinScriptType; +(function (BitcoinScriptType) { + // Pay to Public Key Hash + BitcoinScriptType["P2PKH"] = "pubkeyhash"; + // Pay to Script Hash + BitcoinScriptType["P2SH"] = "scripthash"; + // Pay to Witness Public Key Hash + BitcoinScriptType["P2WPKH"] = "witnesspubkeyhash"; + // Pay to Witness Script Hash + BitcoinScriptType["P2WSH"] = "witnessscripthash"; + // Pay to Taproot + BitcoinScriptType["P2TR"] = "taproot"; +})(BitcoinScriptType || (exports.BitcoinScriptType = BitcoinScriptType = {})); +/** + * Determines the type of Bitcoin script. + * + * This function tries to parse the script using different Bitcoin payment types and returns + * a string identifier for the script type. + * + * @param script - The raw script as a Buffer + * @returns {BitcoinScriptType} The identified script type + * @throws {Error} If the script cannot be identified as any known type + */ +const getScriptType = (script) => { + try { + bitcoinjs_lib_1.payments.p2pkh({ output: script }); + return BitcoinScriptType.P2PKH; + } + catch (_a) { } + try { + bitcoinjs_lib_1.payments.p2sh({ output: script }); + return BitcoinScriptType.P2SH; + } + catch (_b) { } + try { + bitcoinjs_lib_1.payments.p2wpkh({ output: script }); + return BitcoinScriptType.P2WPKH; + } + catch (_c) { } + try { + bitcoinjs_lib_1.payments.p2wsh({ output: script }); + return BitcoinScriptType.P2WSH; + } + catch (_d) { } + try { + bitcoinjs_lib_1.payments.p2tr({ output: script }); + return BitcoinScriptType.P2TR; + } + catch (_e) { } + throw new Error("Unknown script type"); +}; +exports.getScriptType = getScriptType; diff --git a/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md b/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md new file mode 100644 index 0000000000..f952be2a22 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md @@ -0,0 +1,491 @@ +# Advanced BTC Staking Transaction Usage + +> ⚠️ **WARNING**: This documentation describes advanced usage of btc-staking-ts +> where you can customize transaction parameters to fit your specific needs. +> While this offers more flexibility, creating custom Bitcoin transactions +> carries inherent risks. Incorrect parameters or improper usage could result +> in loss of funds. Proceed at your own risk and thoroughly test all transactions +> in a test environment first. + + +This guide demonstrates how to manually construct Bitcoin staking transactions by customizing various parameters such as covenant settings, staking durations, and transaction details. It's intended for developers who need fine-grained control over the Bitcoin transaction aspect of the staking process. It does not cover the Babylon transaction aspect. + + +## Advanced Usage + +### Define Staking Parameters + +To determine the correct parameter version to use, this library provides two utility methods: +- `getBabylonParamByBtcHeight`: Get parameters based on Bitcoin block height +- `getBabylonParamByVersion`: Get parameters based on version number + +These methods ensure you use the appropriate parameter set based on the current state of the Babylon network. + +```ts +import { networks } from "bitcoinjs-lib"; + +// 1. Collect the Babylon system parameters. +// These are parameters that are shared between for all Bitcoin staking +// transactions, and are maintained by Babylon governance. +// They involve: +// - `covenantPks: Buffer[]`: A list of the public keys +// without the coordinate bytes correspondongin to the +// covenant emulators. +// - `covenantThreshold: number`: The amount of covenant +// emulator signatures required for the staking to be activated. +// - `minimumUnbondingTime: number`: The minimum unbonding period +// allowed by the Babylon system . +// - `lockHeight: number`: Indicates the BTC height before which +// the transaction is considered invalid. This value can be derived from +// the `activationHeight` of the Babylon versioned global parameters +// where the current BTC height is. Note that if the +// `current BTC height + 1 + confirmationDepth` is going to be >= +// the next versioned `activationHeight`, then you should use the +// `activationHeight` from the next version of the global parameters. +// Below, these values are hardcoded, but they should be retrieved from the +// Babylon system. +const covenantPks: Buffer[] = covenant_pks.map((pk) => Buffer.from(pk, "hex")); +const covenantThreshold: number = 3; +const minUnbondingTime: number = 101; +// Optional field. Value coming from current global param activationHeight +const lockHeight: number = 0; + +// 2. Define the user selected parameters of the staking contract: +// - `stakerPk: Buffer`: The public key without the coordinate of the +// staker. +// - `finalityProviders: Buffer[]`: A list of public keys without the +// coordinate corresponding to the finality providers. Currently, +// a delegation to only a single finality provider is allowed, +// so the list should contain only a single item. +// - `stakingDuration: number`: The staking period in BTC blocks. +// - `stakingAmount: number`: The amount to be staked in satoshis. +// - `unbondingTime: number`: The unbonding time. Should be `>=` the +// `minUnbondingTime`. + +const stakerPk: Buffer = btcWallet.publicKeyNoCoord(); +const finalityProviders: Buffer[] = [ + Buffer.from(finalityProvider.btc_pk_hex, "hex"), +]; +const stakingDuration: number = 144; +const stakingAmount: number = 1000; +const unbondingTime: number = minUnbondingTime; + +// 3. Define the parameters for the staking transaction that will contain the +// staking contract: +// - `inputUTXOs: UTXO[]`: The list of UTXOs that will be used as an input +// to fund the staking transaction. +// - `feeRate: number`: The fee per tx byte in satoshis. +// - `changeAddress: string`: BTC wallet change address, Taproot or Native +// Segwit. +// - `network: network to work with, either networks.testnet +// for BTC Testnet and BTC Signet, or networks.bitcoin for BTC Mainnet. + +// Each object in the inputUTXOs array represents a single UTXO with the following properties: +// - txid: transaction ID, string +// - vout: output index, number +// - value: value of the UTXO, in satoshis, number +// - scriptPubKey: script which provides the conditions that must be fulfilled for this UTXO to be spent, string +const inputUTXOs = [ + { + txid: "e472d65b0c9c1bac9ffe53708007e57ab830f1bf09af4bfbd17e780b641258fc", + vout: 2, + value: 9265692, + scriptPubKey: "0014505049839bc32f869590adc5650c584e17c917fc", + }, +]; +const feeRate: number = 18; +const changeAddress: string = btcWallet.address; +const network = networks.testnet; +``` + +### Create the Staking Contract + +After defining its parameters, +the staking contract can be created. +First, create an instance of the `StakingScriptData` class +and construct the Bitcoin scipts associated with Bitcoin staking using it. + +```ts +import { StakingScriptData } from "@babylonlabs-io/btc-staking-ts"; + +const stakingScriptData = new StakingScriptData( + stakerPk, + finalityProviders, + covenantPks, + covenantThreshold, + stakingDuration, + minUnbondingTime +); + +const { + timelockScript, + unbondingScript, + slashingScript, + unbondingTimelockScript, +} = stakingScriptData.buildScripts(); +``` + +The above scripts correspond to the following: + +- `timelockScript`: A script that allows the Bitcoin to be retrieved only + through the staker's signature and the staking period being expired. +- `unbondingScript`: The script that allows on-demand unbonding. + Requires the staker's signature and the covenant committee's signatures. +- `slashingScript`: The script that enables slashing. + It requires the staker's signature and in this phase the staker should not sign it. + +### Create a staking transaction + +Using the Bitcoin staking scripts, you can generate a Bitcoin staking +transaction and later sign it using a supported wallet's method. +In this instance, we use the `btcWallet.signTransaction()` method. + +```ts +import { stakingTransaction } from "@babylonlabs-io/btc-staking-ts"; +import { Psbt, Transaction } from "bitcoinjs-lib"; + +// stakingTransaction constructs an unsigned BTC Staking transaction +const unsignedStakingPsbt: {psbt: Psbt, fee: number} = stakingTransaction( + scripts: { + timelockScript, + unbondingScript, + slashingScript, + }, + stakingAmount, + changeAddress, + inputUTXOs, + network(), + feeRate, + btcWallet.isTaproot ? btcWallet.publicKeyNoCoord() : undefined, + lockHeight, +); + +const signedStakingPsbt = await btcWallet.signPsbt(unsignedStakingPsbt.psbt.toHex()); +const stakingTx = Psbt.fromHex(signedStakingPsbt).extractTransaction(); +``` + +Public key is needed only if the wallet is in Taproot mode, for `tapInternalKey`. + +### Create staking expansion transaction + +Staking expansion allows you to extend an existing BTC stake with additional +finality providers or renew the timelock without going through the full +unbonding process. + +The expansion transaction: +1. Spends the previous staking transaction output as the first input. +2. Uses a funding UTXO as the second input to cover transaction fees. +3. Creates new staking outputs where the timelock is renewed or additional +finality providers are added. +4. Returns any remaining funds as change. + +```ts +import { stakingExpansionTransaction } from "@babylonlabs-io/btc-staking-ts"; +import { Psbt, Transaction } from "bitcoinjs-lib"; + +// Previous staking transaction that we want to expand +const previousStakingTx: Transaction = stakingTx; // from previous staking + +// Scripts from the previous staking transaction +const previousStakingScripts = { + timelockScript, + unbondingScript, + slashingScript, +}; + +// New scripts for the expansion. The finality providers of this scripts must +// include ALL finality providers from the previous staking transaction. +// Additional finality providers can be added, but none can be removed. +// The timelock script can be renewed or left unchanged. +const expansionScripts = { + timelockScript: newTimelockScript, + unbondingScript: newUnbondingScript, + slashingScript: newSlashingScript, +}; + +// Funding UTXOs to cover transaction fees. +// The funding UTXOs are used only to cover transaction fees, not to increase +// the staking amount (this feature is not yet supported). +// Any remaining funds from the funding UTXOs will be returned as change. +// The method automatically selects the funding UTXO that can cover the +// transaction fees, so a single funding UTXO is sufficient. +const fundingUTXOs = [ + { + txid: "e472d65b0c9c1bac9ffe53708007e57ab830f1bf09af4bfbd17e780b641258fc", + vout: 2, + value: 9265692, + scriptPubKey: "0014505049839bc32f869590adc5650c584e17c917fc", + }, +]; + +const expansionResult = stakingExpansionTransaction( + network, + expansionScripts, + stakingAmount, // Must equal the previous staking amount + changeAddress, + feeRate, + fundingUTXOs, + { + stakingTx: previousStakingTx, + scripts: previousStakingScripts, + } +); + +const { + transaction: stakingExpansionTx, + fee: expansionFee, + fundingUTXO, // The selected funding UTXO that covers the transaction fees +} = expansionResult; + +// Sign the expansion transaction +const signedExpansionPsbt = await btcWallet.signPsbt(stakingExpansionTx.toHex()); +const signedExpansionTx = Psbt.fromHex(signedExpansionPsbt).extractTransaction(); +``` + +**Important Notes:** +- The expansion amount must equal the previous staking amount (increases are not yet supported) +- The finality providers used to construct the expansion staking transaction scripts must be a superset of those from the previous staking (all previous finality providers must be included, with additional ones allowed) +- The expansion transaction requires covenant signatures to spend the previous staking output +- The funding UTXO is used only to cover transaction fees, not to increase the staking amount + +#### Collecting Expansion Covenant Signatures + +Similar to unbonding transactions, staking expansion requires covenant signatures to spend the previous staking output: + +```ts +// Create the full witness with covenant signatures +const witness = createCovenantWitness( + signedExpansionTx.ins[0].witness, // original witness from signed transaction + covenantPks: Buffer[], + covenantExpansionSignatures: { + btc_pk_hex: string; + sig_hex: string; + }[], + covenantQuorum +); + +// Attach the witness to the expansion transaction +signedExpansionTx.ins[0].witness = witness; +``` + +### Create unbonding transaction + +The staking script allows users to on-demand unbond their locked stake before +the staking transaction timelock expires, subject to an unbonding period. + +The unbonding transaction can be created as follows: + +```ts +import { unbondingTransaction } from "@babylonlabs-io/btc-staking-ts"; +import { Psbt, Transaction } from "bitcoinjs-lib"; + +// Unbonding fee in satoshis. number +const unbondingFee: number = 500; + +const unsignedUnbondingPsbt: {psbt: Psbt} = unbondingTransaction( + scripts: { + unbondingTimelockScript, + slashingScript, + }, + stakingTx, + unbondingFee, + network, + outputIndex // The staking transaction output path index +); + +const signedUnbondingPsbt = await signPsbt(unsignedUnbondingPsbt.psbt.toHex()); +const unbondingTx = Psbt.fromHex(signedUnbondingPsbt).extractTransaction(); +``` + +#### Collecting Unbonding Signatures + +The unbonding transaction requires two types of signatures to be valid and +acceptable by the Bitcoin network: +1. The staker's signature +2. The covenant committee signatures + +To obtain a complete, valid unbonding transaction that can be submitted to +Bitcoin, you'll need to retrieve the covenant signatures from the Babylon +network after: +- Your delegation has been successfully registered on Babylon +- The covenant committee has verified your delegation + +You can obtain these covenant signatures either by: +- Querying the Babylon node directly +- Using the Babylon API endpoints + +Once you have both the staker's signature and the covenant signatures, +you can combine them like this: + +```ts +// Create the full witness +const witness = createCovenantWitness( + unbondingTx.ins[0].witness: Buffer[], // original witness + covenantPks: Buffer[], + covenantUnbondingSignatures: { + btc_pk_hex: string; + sig_hex: string; + }[], + covenantQuorum +); + +// Put the witness inside the unbonding transaction. +unbondingTx.ins[0].witness = witness;; +``` + +### Withdrawing + +Withdrawing involves extracting funds for which the staking/unbonding period has expired from the staking/unbonding transaction. + +Initially, we specify the withdrawal transaction parameters. + +```ts +// The index of the staking/unbonding output in the staking/unbonding +// transcation. +const stakingOutputIndex: number = 0; + +// The fee that the withdrawl transaction should use. +const withdrawalFee: number = 500; + +// The address to which the funds should be withdrawed to. +const withdrawalAddress: string = btcWallet.address; +``` + +Then, we construct the withdrawal transaction. +There are three types of withdrawal + +1. Withdraw funds from a staking transaction in which the timelock naturally expired: + +```ts +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { withdrawTimelockUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; + +// staking transaction. Transaction +const stakingTx: Transaction = undefined; + +const unsignedWithdrawalPsbt: {psbt: Psbt, fee: number} = withdrawTimelockUnbondedTransaction( + scripts: { + timelockScript, + slashingScript, + unbondingScript, + }, + stakingTx, + btcWallet.address, + network, + feeRate, + stakingOutputIndex, +); +``` + +2. Withdraw funds from an unbonding transaction that was submitted for early unbonding and the unbonding period has passed: + +```ts +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { withdrawEarlyUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; + +const unsignedWithdrawalPsbt: { psbt: Psbt, fee: number } = withdrawEarlyUnbondedTransaction( + scripts: { + unbondingTimelockScript, + slashingScript, + }, + unbondingTx, + withdrawalAddress, + network, + feeRate, +); + +const signedWithdrawalPsbt = await signPsbt(unsignedWithdrawalPsbt.psbt.toHex()); +const withdrawalTransaction = Psbt.fromHex(signedWithdrawalPsbt).extractTransaction(); +``` + +3. Withdraw from a slashed transaction where its timelock has expired + +```ts +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { withdrawSlashingTransaction } from "@babylonlabs-io/btc-staking-ts"; + + +const unsignedWithdrawalPsbt: { psbt: Psbt, fee: number } = withdrawSlashingTransaction( + scripts: { + unbondingTimelockScript, + }, + slashingTx, + withdrawalAddress, + network, + feeRate, + outputIndex // the output index from the slashing tx +); + +const signedWithdrawalPsbt = await signPsbt(unsignedWithdrawalPsbt.psbt.toHex()); +const withdrawalTransaction = Psbt.fromHex(signedWithdrawalPsbt).extractTransaction(); +``` + +### Create slashing transaction + +The slashing transaction is the transaction that is sent to Bitcoin in the event of the finality provider in which the stake has been delegated to performs an offence. + +First, collect the parameters related to slashing. +These are Babylon parameters and should be collected from the Babylon system. + +```ts +// The public key script to send the slashed funds to. +const slashingPkScriptHex: string = ""; +// 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; +``` + +Then create and sign the slashing transaction. +There are two types of slashing transactions: + +1. Slashing of the staking transaction when no unbonding has been performed: + +```ts +import { slashTimelockUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; +import { Psbt, Transaction } from "bitcoinjs-lib"; + +const outputIndex: number = 0; + +const unsignedSlashingPsbt: {psbt: Psbt} = slashTimelockUnbondedTransaction( + scripts: { + slashingScript, + unbondingScript, + timelockScript, + unbondingTimelockScript, + }, + stakingTx, + slashingPkScriptHex, + slashingRate, + minimumSlashingFee, + network, + outputIndex, +); + +const signedSlashingPsbt = await signPsbt(unsignedSlashingPsbt.psbt.toHex()); +const slashingTx = Psbt.fromHex(signedSlashingPsbt).extractTransaction(); +``` + +2. Slashing of the unbonding transaction in the case of on-demand unbonding: + +create unsigned unbonding slashing transaction + +```ts +import { Psbt, Transaction } from "bitcoinjs-lib"; +import { slashEarlyUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; + +const unsignedUnbondingSlashingPsbt: {psbt: Psbt} = slashEarlyUnbondedTransaction( + scripts: { + slashingScript, + unbondingTimelockScript, + }, + unbondingTx, + slashingPkScriptHex, + slashingRate, + minimumSlashingFee, + network, +); + +const signedUnbondingSlashingPsbt = await signPsbt(unsignedUnbondingSlashingPsbt.psbt.toHex()); +const unbondingSlashingTx = Psbt.fromHex(signedUnbondingSlashingPsbt).extractTransaction(); +``` diff --git a/modules/babylonlabs-io-btc-staking-ts/docs/usage.md b/modules/babylonlabs-io-btc-staking-ts/docs/usage.md new file mode 100644 index 0000000000..a740e9f3dd --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/docs/usage.md @@ -0,0 +1,517 @@ +# Babylon Bitcoin Staking - Usage Guide + +This document describes how to use the Bitcoin Staking +TypeScript library to generate the Bitcoin and Babylon Genesis +transactions associated with the Bitcoin staking protocol. +For a comprehensive review of the protocol for the registration +of Bitcoin stakes on the Babylon Genesis chain and the transactions involved, +please read the [stake registration +documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md). + +## Table of Contents + +- [Babylon Bitcoin Staking - Usage Guide](#babylon-bitcoin-staking---usage-guide) + - [Table of Contents](#table-of-contents) + - [1. Prerequisites](#1-prerequisites) + - [1.1 Staking Parameters](#11-staking-parameters) + - [1.2 Bitcoin Staker information](#12-bitcoin-staker-information) + - [1.3 Signing Providers](#13-signing-providers) + - [2. Staking Manager Initialization](#2-staking-manager-initialization) + - [3. Stake Registration](#3-stake-registration) + - [3.1 Post-Staking Registration](#31-post-staking-registration) + - [3.2 Pre-Staking Registration](#32-pre-staking-registration) + - [4. Delegation Expansion](#4-delegation-expansion) + - [4.1 Staking Expansion Registration](#41-staking-expansion-registration) + - [4.2 Create Signed Staking Expansion Transaction](#42-create-signed-staking-expansion-transaction) + - [5. Unbonding Transaction](#5-unbonding-transaction) + - [6. Withdrawal Transaction](#6-withdrawal-transaction) + - [7. Fee Calculation](#7-fee-calculation) + - [7.1 Bitcoin Transaction Fee](#71-bitcoin-transaction-fee) + - [7.2 Babylon Genesis Transaction Fee](#72-babylon-genesis-transaction-fee) + +## 1. Prerequisites + +### 1.1 Staking Parameters + +The Bitcoin Staking parameters define the conditions +that the Bitcoin Staking script and the Bitcoin Staking +transaction containing it must satisfy. +They are versioned parameters, with each version corresponding +to a range of Bitcoin heights. + +You can retrieve the parameters as follows: +* By querying the `/babylon/btcstaking/v1/params` endpoint + of an RPC/LCD node. You can find the available RPC/LCD nodes + of each active network in the + [Babylon networks Information](https://docs.babylonlabs.io/developers/babylon_genesis_chain/node_information/). +* By querying the `/v2/network-info` endpoint of the + [Babylon Staking API](https://docs.babylonlabs.io/api/staking-api/get-network-info/) + that exposes the indexed Babylon parameters. + +To learn more about the Bitcoin staking parameters and their usage in +constructing and validating Bitcoin Staking transactions, please refer to the +[specification](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md#32-babylon-chain-btc-staking-parameters). + +> **Important**: The parameters are subject to change as they are controlled +> by Babylon Genesis governance. Please make sure that you understand how to +> select the correct parameters before creating your Bitcoin Staking +> transaction. Usage of the incorrect parameters might lead to (temporary or not) +> fund loss. + +### 1.2 Bitcoin Staker information + +**Staker's Bitcoin Details** + +The staker's *Bitcoin address* serves two key purposes within this library: +- Acts as the change address when creating Bitcoin staking transactions +- Serves as the withdrawal address when creating Bitcoin withdrawal transactions + +The staker's *Bitcoin public key* is essential throughout the creation of all +Bitcoin transactions as it is used to construct the tapscript. + +> **Important**: The Bitcoin address should always correspond to the Bitcoin +> public key and be generated by it. + +Staker inputs define the parameters for Bitcoin staking. Stakers can customize +their: +- Staking timelock duration +- Staking amount +- Selected finality providers for delegation + +> **Note**: The other parameters included in the +> [Bitcoin Staking script](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/staking-script.md) +> are retrieved through the Bitcoin Staking parameters. + +```ts +const stakerInfo = { + // BTC Address + address: string, + // BTC compressed public Key in the 32-byte x-coordinate only hex format. + publicKeyNoCoordHex: string, +} + +const stakerInput = { + // The chosen finality providers public keys in compressed 32 bytes x-coordinate + // only format. + finalityProviderPksNoCoordHex: string[], + // Amount of satoshis staker choose to stake + stakingAmountSat: number, + // The number of BTC blocks this staking transaction will be staked for + stakingTimelock: number +} +``` + +**Staker's Babylon Genesis Details** + +The Babylon Genesis address specifies the address of the staker on the Babylon +Genesis blockchain. It is used as the signer of the +Babylon Genesis transaction to register the stake and +to create the Proof of Possession (PoP), +which must be signed during the registration process. +The PoP is used to confirm the ownership of the Bitcoin key used for staking +by the Babylon Genesis account used for stake registration. + +```ts +// Babylon bech32 address with prefix of `bbn`. +const babylonAddress = string +``` + +### 1.3 Signing Providers + +A Provider is a construct that maintains a private key that can be used for +signing operations (e.g., a wallet). + +For the purposes of this library, two providers are required: +- **Bitcoin Provider**: Responsible for signing Bitcoin transactions and +arbitrary messages through the ECDSA or BIP-322 algorithms. +- **Babylon Genesis Provider**: Responsible for signing Babylon Genesis +transactions. Babylon Genesis is based on Cosmos SDK, so providers that +support Cosmos SDK chains should be straightforward to adapt to Babylon Genesis. + +Below we define the expected interface for both of the above providers. + +```ts +export interface BtcProvider { + // Sign a PSBT + signPsbt( + psbtHex: string, + options?: SignPsbtOptions + ): Promise; + // Sign a message using the ECDSA type + // This is optional and only required if you would like to use the + // `createProofOfPossession` function + signMessage?: ( + message: string, type: "ecdsa" || "bip322-simple" + ) => Promise; +} + +export interface BabylonProvider { + // Signs a Babylon chain transaction. + // This is primarily used for signing MsgCreateBTCDelegation transactions + // which register the BTC delegation on the Babylon Genesis chain. + signTransaction: ( + msg: { + typeUrl: string; + value: T; + } + ) => Promise +} +``` + +## 2. Staking Manager Initialization + +To use the library, you'll need to create an instance +of `BabylonBtcStakingManager` with the following required parameters: + +```ts +import { BabylonBtcStakingManager } from "@babylonlabs-io/btc-staking-ts"; + +const manager = new BabylonBtcStakingManager( + btcNetwork, // Bitcoin network configuration (mainnet or testnet) + stakingParams, // Staking parameters retrieved as described in Prerequisites + btcProvider, // Bitcoin Provider for signing Bitcoin transactions + bbnProvider // Babylon Provider for signing Babylon Genesis transactions +); +``` + +The manager instance provides the necessary methods for creating and +managing Bitcoin staking transactions. Make sure you have all the prerequisites +(staking parameters and providers) properly configured before initializing the +manager. + +## 3. Stake Registration + +The Bitcoin staker utilizes the staking inputs +(stake amount, timelock, finality providers) together +with the staking parameters to construct the necessary +transactiosn required by the Bitcoin Staking protocol. + +These transactions include: +- BTC Staking Transaction: The Bitcoin transaction that locks the stake in + the self-custodial Bitcoin staking script. +- Slashing Transaction: A pre-signed transaction consenting to slashing in + case of double-signing. +- Unbonding Transaction: The on-demand unbonding transaction used to unlock + the stake before the originally committed timelock expires. +- Unbonding Slashing Transaction: A pre-signed transaction consenting to + slashing during the unbonding process in case of double-signing. + +There are two types of registrations supported by Babylon Genesis chain in which +they fits for difference purpose/use-case: + +- **Post-Staking Registration**: Should be used by stakers that already have + their Bitcoin Staking transaction included in the Bitcoin ledger (e.g., + phase-1 stakers) +- **Pre-Staking Registration**: Can be used by stakers that prefer to receive + the required stake verification guarantees before locking their funds on + Bitcoin. This is the recommended method for staking for new Babylon Genesis + stakes (i.e., ones created from phase-2 onwards). + +For more details about the two types, refer to the +[Babylon node documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md#2-bitcoin-stake-registration-methods). + +> **Important**: Phase-1 stakers should always use the post-staking +> registration method to register their phase-1 stakes. For newly created +> phase-2 stakes, both methods can be used. + + +### 3.1 Post-Staking Registration + +This flow is for stakers who already have a confirmed BTC staking transaction +(`k`-blocks deep, where `k` is defined in the staking parameters) +and want to register it on the Babylon chain. +This process is particularly suitable for Babylon Phase-1 stakes. + +The post-staking registration consists of multiple transactions and messages, +some requiring signatures from the BTC Provider: +- Bitcoin staking transaction (already on the Bitcoin network) and a proof of + inclusion +- Bitcoin unbonding transaction +- Bitcoin slashing transaction (**requires signing**) +- Bitcoin slashing unbonding transaction (**requires signing**) +- Proof of Possession (**requires signing**) + +The final constructed message will be signed by the Babylon Provider as a +Babylon Genesis transaction, ready for submission to the network. + +```ts +const { + signedBabylonTx +} = await manager.postStakeRegistrationBabylonTransaction( + stakerInfo, + stakingTx, + stakingHeight, + stakingInput, + inclusionProof, + bech32Address, +); +``` + +### 3.2 Pre-Staking Registration + +The Pre-staking registration flow is for stakers who seek verification from the +Babylon chain before submitting their BTC staking transaction to the Bitcoin +ledger. It is a multi-step process, involving: +1. The registration of the stake on the Babylon Genesis chain. +2. After verification is received, the submission of the signed Bitcoin + staking transaction to the Bitcoin ledger. +3. Once the Bitcoin staking transaction has received sufficient confirmations, + the Babylon Genesis chain is notified about its inclusion. + +The registration to the Babylon Genesis blockchain +consists of the submission of multiple transactions and messages, some requiring +signatures from the BTC Provider: +- Bitcoin staking transaction +- Bitcoin unbonding transaction +- Bitcoin slashing transaction (**requires signing**) +- Bitcoin slashing unbonding transaction (**requires signing**) +- Proof of Possession (**requires signing**) + +First, we create the initial registration message that will be signed +by the Babylon Provider as a Babylon Genesis transaction, +ready for submission to the network. + +```ts +const { + signedBabylonTx +} = await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + babylonBtcTipHeight, + inputUTXOs, + feeRate, + bech32Address, +); +``` + +Next, we monitor for the transaction receiving verification by the Babylon +blockchain. We can do so by querying the Babylon node and checking for the +`VERIFIED` status as follows: +- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` + endpoint of an RPC/LCD node. For more details, see this + [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). +- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the + Babylon Staking API. For more details, see this + [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). + +After the stake has been marked as verified by the Babylon Genesis chain, +we can construct the Bitcoin Staking transaction that is ready to be +broadcast to the Bitcoin ledger. +```ts +const signedBtcStakingTx = await manager.createSignedBtcStakingTransaction({ + stakerInfo, + stakingInput, + unsignedStakingTx, + inputUTXOs, + stakingParamsVersion +}) +``` + +Once the staking transaction has been included in the Bitcoin ledger with +sufficient confirmations, an off-chain program called the +[vigilante BTC Staking tracker](https://github.com/babylonlabs-io/vigilante) +notifies the Babylon chain about the staking transaction's inclusion. + +## 4. Delegation Expansion + +Delegation expansion allows you to extend an existing BTC stake with additional +finality providers or renew the timelock without going through the full unbonding +process. For more details, please refer to the +[BTC Stake Expansion](https://github.com/babylonlabs-io/babylon/blob/v3.0.0-rc.1/docs/bitcoin-stake-expansion.md). + +The expansion process involves: +1. Creating an unsigned staking expansion transaction +2. Registering the expansion on the Babylon Genesis chain +3. Signing and submitting the expansion transaction to Bitcoin + +### 4.1 Staking Expansion Registration + +First, create the initial expansion registration message that will be signed +by the Babylon Provider as a Babylon Genesis transaction. + +```ts +const { + signedBabylonTx, + stakingTx: stakingExpansionTx +} = await manager.stakingExpansionRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + babylonBtcTipHeight, + inputUTXOs, + feeRate, + babylonAddress, + { + stakingTx: previousStakingTx, + paramVersion: previousStakingParamsVersion, + stakingInput: previousStakingInput, + } +); +``` + +**Important Notes:** +- The expansion amount must equal the previous staking amount (increases are +not yet supported). +- All finality providers from the previous staking must be included in the +expansion. You can retrieve the previous staking information through the +`/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the Babylon Staking API. +- The input UTXOs are used only to cover transaction fees, not to increase +the staking amount (this feature is not yet supported). The method +automatically selects the optimal UTXO to cover the fees. + +### 4.2 Create Signed Staking Expansion Transaction + +After the expansion has been registered and verified by the Babylon chain, +you can create the signed staking expansion transaction. This requires +covenant signatures from the covenant committee. + +You can retrieve the covenant expansion signatures through either: +- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` + endpoint of an RPC/LCD node +- By querying the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the + Babylon Staking API + +```ts +const signedStakingExpansionTx = await manager.createSignedBtcStakingExpansionTransaction( + stakerInfo, + stakingInput, + unsignedStakingExpansionTx, + inputUTXOs, + stakingParamsVersion, + { + stakingTx: previousStakingTx, + paramVersion: previousStakingParamsVersion, + stakingInput: previousStakingInput, + }, + covenantStakingExpansionSignatures +); +``` + +The resulting transaction is ready to be submitted to the Bitcoin network. + +## 5. Unbonding Transaction + +This step allows stakers to unbond their active staking transactions on demand +before the committed timelock expires. After unbonding, the funds will become +available for withdrawal once the unbonding period +(specified in the staking parameters) has elapsed. + +The unbonding transaction requires signatures from both the staker and the +covenant committee. This step combines these signatures to create a complete +transaction ready for submission to the Bitcoin network. + +You can retrieve the unsigned unbonding transaction and covenant committee +signatures through either: +- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` + endpoint of an RPC/LCD node. For more details, see this + [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). +- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the + Babylon Staking API. For more details, see this + [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). + +```ts +const { signedUnbondingTx } = await manager.createSignedBtcUnbondingTransaction({ + stakerInfo, + stakingInput, + stakingParamsVersion, + stakingTx, + unsignedUnbondingTx, + covenantUnbondingSignatures +}) +``` + +## 6. Withdrawal Transaction + +There are 3 different types of withdrawal transactions: +1. Withdraw from early unbonding (`createSignedBtcWithdrawEarlyUnbondedTransaction`) + - Used when the unbonding period has passed +2. Withdraw from expired timelock (`createSignedBtcWithdrawStakingExpiredTransaction`) + - Used when the staking period has naturally ended +3. Withdraw from slashed stake (`createSignedBtcWithdrawSlashingTransaction`) + - Used when withdrawing slashed funds after timelock expiry + +All withdrawal transactions will direct the change balance to the staker's +address (provided via `stakerInfo`). +For more customized transaction options, +please refer to the [advanced usage documentation](docs/advanced-btc-tx.md). + +The required input transaction varies depending on the withdrawal method: +- Early unbonding withdrawal requires the unbonding transaction +- Timelock expiry withdrawal requires the staking transaction +- Slashed stake withdrawal requires the slashing transaction + +You can retrieve the Bitcoin staking transaction, unsigned unbonding transaction +, slashing transaction and staking input made at the time of creating the staking +transaction through either: +- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` + endpoint of an RPC/LCD node. For more details, see this + [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). +- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the + Babylon Staking API. For more details, see this + [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). + + +```ts +// 1. Withdraw from early unbonding (when unbonding period has passed) +const signedWithdrawEarlyUnbondedTx = await manager.createSignedBtcWithdrawEarlyUnbondedTransaction({ + stakerInfo, + stakingInput, + stakingParamsVersion, + unbondingTx, // Withdraw from unbonding transaction + feeRate +}) + +// 2. Withdraw from expired timelock (when staking period has naturally ended) +const signedWithdrawTimelockExpiredTx = await manager.createSignedBtcWithdrawStakingExpiredTransaction({ + stakerInfo, + stakingInput, + stakingParamsVersion, + stakingTx, // Withdraw from staking transaction + feeRate +}) + +// 3. Withdraw from slashed Bitcoin Staking transaction (after timelock expiry) +const signedWithdrawSlashedTx = await manager.createSignedBtcWithdrawSlashingTransaction({ + stakerInfo, + stakingInput, + stakingParamsVersion, + slashingTx, // Withdraw from slashing transaction + feeRate +}) +``` + +## 7. Fee Calculation + +### 7.1 Bitcoin Transaction Fee + +The library's fee calculation for Bitcoin transactions is based on an estimated +size of the transaction in virtual bytes (vB). This estimation helps in +calculating the appropriate fee to include in the transaction to ensure it is +processed by the Bitcoin network efficiently. + +> **Note**: The fee estimation is only used for transactions in which the +> protocol allows to specify a custom fee, i.e., the staking and withdrawal +> transactions. The slashing and unbonding transactions have a pre-defined fee +> amount that should be used based on the Bitcoin Staking parameters utilized +> for the staking operation. Please refer to the +> [staking registration documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md) +> for more details. + +```ts +// Calculate the estimated fee for a staking transaction +const feeSats = manager.estimateBtcStakingFee({ + stakerInfo, + babylonBtcTipHeight, + stakingInput, + inputUTXOs, + feeRate +}) +``` + +### 7.2 Babylon Genesis Transaction Fee + +The current version of the library does support functionality for calculating +the Babylon Genesis transaction fees for the `pre-staking registration` +and `post-staking registration` operations. This feature will be added in a +future release. +For now please refer to the +[simple-staking example](https://github.com/babylonlabs-io/simple-staking/blob/main/src/app/hooks/client/rpc/mutation/useBbnTransaction.ts#L27). diff --git a/modules/babylonlabs-io-btc-staking-ts/jest.setup.js b/modules/babylonlabs-io-btc-staking-ts/jest.setup.js index eab17e50a6..1f446f4b85 100644 --- a/modules/babylonlabs-io-btc-staking-ts/jest.setup.js +++ b/modules/babylonlabs-io-btc-staking-ts/jest.setup.js @@ -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(); diff --git a/modules/babylonlabs-io-btc-staking-ts/package.json b/modules/babylonlabs-io-btc-staking-ts/package.json index af54fd71a8..dcd27b53be 100644 --- a/modules/babylonlabs-io-btc-staking-ts/package.json +++ b/modules/babylonlabs-io-btc-staking-ts/package.json @@ -1,20 +1,21 @@ { - "name": "@bitgo/babylonlabs-io-btc-staking-ts", - "version": "2.4.1", + "name": "@babylonlabs-io/btc-staking-ts", + "version": "0.0.0-semantic-release", "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", "typings": "dist/index.d.ts", "type": "module", - "exports": { - "import": "./dist/index.js", - "require": "./dist/index.cjs" - }, "scripts": { - "generate-types": "dts-bundle-generator -o ./dist/index.d.cts ./src/index.ts --no-check", + "generate-types": "dts-bundle-generator -o ./dist/index.d.ts ./src/index.ts", "build": "node build.js && npm run generate-types", + "format": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", + "format:fix": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", + "lint": "eslint ./src --fix", "prepare": "husky", "prepublishOnly": "npm run build", + "test": "jest --verbose", + "test:watch": "jest --watch", "version:canary": "npm version prerelease --preid=canary" }, "files": [ @@ -27,21 +28,56 @@ "btc-staking" ], "engines": { - "node": ">=20 < 23" + "node": ">=22.0.0 <25.0.0" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.ts": [ + "prettier --write", + "eslint --fix" + ], + "tests/**/*.ts": [ + "prettier --write", + "eslint --fix" + ] }, "author": "Babylon Labs Ltd.", "license": "SEE LICENSE IN LICENSE", "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@semantic-release/npm": "^12.0.1", + "@types/jest": "^29.5.12", + "@types/node": "^20.11.30", + "@typescript-eslint/parser": "^7.4.0", "dts-bundle-generator": "^9.3.1", - "esbuild": "^0.20.2", - "nanoevents": "^9.1.0" + "ecpair": "^2.1.0", + "esbuild": "^0.25.9", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "husky": "^9.0.11", + "jest": "^29.7.0", + "lint-staged": "^15.2.7", + "nanoevents": "^9.1.0", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^3.2.4", + "semantic-release": "^24.2.3", + "ts-jest": "^29.1.4", + "typescript": "^5.4.5", + "typescript-eslint": "^7.4.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", - "bitcoinjs-lib": "^6.1.7" + "bitcoinjs-lib": "6.1.5" }, "publishConfig": { "access": "public" diff --git a/modules/babylonlabs-io-btc-staking-ts/src/constants/fee.ts b/modules/babylonlabs-io-btc-staking-ts/src/constants/fee.ts index f68caf697a..1e89916cab 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/constants/fee.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/constants/fee.ts @@ -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 diff --git a/modules/babylonlabs-io-btc-staking-ts/src/constants/registry.ts b/modules/babylonlabs-io-btc-staking-ts/src/constants/registry.ts index ecea14e494..85eedf87eb 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/constants/registry.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/constants/registry.ts @@ -1,3 +1,4 @@ export const BABYLON_REGISTRY_TYPE_URLS = { MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", + MsgBtcStakeExpand: "/babylon.btcstaking.v1.MsgBtcStakeExpand", }; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/constants/staking.ts b/modules/babylonlabs-io-btc-staking-ts/src/constants/staking.ts new file mode 100644 index 0000000000..3cd2476e80 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/src/constants/staking.ts @@ -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"; \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/src/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/index.ts index 3f151a34df..f28cccdf56 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/index.ts @@ -15,7 +15,3 @@ export { export * from "./utils/utxo/findInputUTXO"; export * from "./utils/utxo/getPsbtInputFields"; export * from "./utils/utxo/getScriptType"; - -// BitGo-specific exports -export * from "./utils/babylon"; -export * from "./utils/staking"; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/index.ts index e3f547be95..d3bec931d3 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/index.ts @@ -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, @@ -28,6 +26,7 @@ import { withdrawSlashingTransaction, withdrawTimelockUnbondedTransaction, } from "./transactions"; +import { validateParams, validateStakingExpansionCovenantQuorum, validateStakingTimelock, validateStakingTxInputData } from "../utils/staking/validation"; export * from "./stakingScript"; export interface StakerInfo { @@ -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. * @@ -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. * diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts index 390d376998..099d6f2921 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts @@ -23,28 +23,38 @@ import { BtcProvider, InclusionProof, StakingInputs, + UpgradeConfig, } from "../types/manager"; import { StakingParams, VersionedStakingParams } from "../types/params"; import { reverseBuffer } from "../utils"; import { isValidBabylonAddress } from "../utils/babylon"; import { isNativeSegwit, isTaproot } from "../utils/btc"; +import { buildPopMessage } from "../utils/pop"; import { + clearTxSignatures, + deriveMerkleProof, deriveStakingOutputInfo, + extractFirstSchnorrSignatureFromTransaction, findMatchingTxOutputIndex, } from "../utils/staking"; import { getBabylonParamByBtcHeight, getBabylonParamByVersion, } from "../utils/staking/param"; + import { createCovenantWitness } from "./transactions"; +import { validateStakingExpansionInputs } from "../utils/staking/validation"; export class BabylonBtcStakingManager { + private upgradeConfig?: UpgradeConfig; + constructor( - protected network: networks.Network, - protected stakingParams: VersionedStakingParams[], - protected btcProvider: BtcProvider, - protected babylonProvider: BabylonProvider, - protected ee?: Emitter + private network: networks.Network, + private stakingParams: VersionedStakingParams[], + private btcProvider: BtcProvider, + private babylonProvider: BabylonProvider, + private ee?: Emitter, + upgradeConfig?: UpgradeConfig, ) { this.network = network; @@ -52,6 +62,8 @@ export class BabylonBtcStakingManager { throw new Error("No staking parameters provided"); } this.stakingParams = stakingParams; + + this.upgradeConfig = upgradeConfig; } /** @@ -133,6 +145,183 @@ export class BabylonBtcStakingManager { }; } + /** + * Create a signed staking expansion transaction that is ready to be sent to + * the Babylon chain. + */ + async stakingExpansionRegistrationBabylonTransaction( + stakerBtcInfo: StakerInfo, + stakingInput: StakingInputs, + babylonBtcTipHeight: number, + inputUTXOs: UTXO[], + feeRate: number, + babylonAddress: string, + // Previous staking transaction info + previousStakingTxInfo: { + stakingTx: Transaction, + paramVersion: number, + stakingInput: StakingInputs, + }, + ): Promise<{ + signedBabylonTx: Uint8Array; + stakingTx: Transaction; + }> { + // Perform validation for the staking expansion inputs + validateStakingExpansionInputs( + { + babylonBtcTipHeight, + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + babylonAddress, + }, + ); + // Param for the expandsion staking transaction + const params = getBabylonParamByBtcHeight( + babylonBtcTipHeight, + this.stakingParams, + ); + + const paramsForPreviousStakingTx = getBabylonParamByVersion( + previousStakingTxInfo.paramVersion, + this.stakingParams, + ); + + const stakingInstance = new Staking( + this.network, + stakerBtcInfo, + params, + stakingInput.finalityProviderPksNoCoordHex, + stakingInput.stakingTimelock, + ); + + const { + transaction: stakingExpansionTx, + fundingUTXO, + } = stakingInstance.createStakingExpansionTransaction( + stakingInput.stakingAmountSat, + inputUTXOs, + feeRate, + paramsForPreviousStakingTx, + previousStakingTxInfo, + ); + let fundingTx; + try { + fundingTx = await this.btcProvider.getTransactionHex(fundingUTXO.txid); + } catch (error) { + throw StakingError.fromUnknown( + error, + StakingErrorCode.INVALID_INPUT, + "Failed to retrieve funding transaction hex", + ); + } + + // Create delegation message without including inclusion proof + const msg = await this.createBtcDelegationMsg( + "delegation:expand", + stakingInstance, + stakingInput, + stakingExpansionTx, + babylonAddress, + stakerBtcInfo, + params, + { + delegationExpansionInfo: { + previousStakingTx: previousStakingTxInfo.stakingTx, + fundingTx: Transaction.fromHex(fundingTx), + }, + }, + ); + + this.ee?.emit("delegation:expand", { + type: "create-btc-delegation-msg", + }); + + return { + signedBabylonTx: await this.babylonProvider.signTransaction(msg), + stakingTx: stakingExpansionTx, + }; + } + + /** + * Estimates the transaction fee for a BTC staking expansion transaction. + * + * @param {StakerInfo} stakerBtcInfo - The staker's Bitcoin information + * including address and public key + * @param {number} babylonBtcTipHeight - The current Babylon BTC tip height + * used to determine staking parameters + * @param {StakingInputs} stakingInput - The new staking input parameters for + * the expansion + * @param {UTXO[]} inputUTXOs - Available UTXOs that can be used for funding + * the expansion transaction + * @param {number} feeRate - Fee rate in satoshis per byte for the expansion + * transaction + * @param {Object} previousStakingTxInfo - Information about the previous + * staking transaction being expanded + * @returns {number} - The estimated transaction fee in satoshis + * @throws {Error} - If validation fails or the fee cannot be calculated + */ + estimateBtcStakingExpansionFee( + stakerBtcInfo: StakerInfo, + babylonBtcTipHeight: number, + stakingInput: StakingInputs, + inputUTXOs: UTXO[], + feeRate: number, + previousStakingTxInfo: { + stakingTx: Transaction, + paramVersion: number, + stakingInput: StakingInputs, + }, + ): number { + // Validate all input parameters before fee calculation + validateStakingExpansionInputs( + { + babylonBtcTipHeight, + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + }, + ); + + // Get the appropriate staking parameters based on the current Babylon BTC + // tip height. This ensures we use the correct parameters for the current + // network state + const params = getBabylonParamByBtcHeight( + babylonBtcTipHeight, + this.stakingParams, + ); + + // Get the staking parameters that were used in the previous staking + // transaction. This is needed to properly reconstruct the previous staking + // scripts + const paramsForPreviousStakingTx = getBabylonParamByVersion( + previousStakingTxInfo.paramVersion, + this.stakingParams, + ); + + // Create a Staking instance for the new expansion with current parameters + // This will be used to build the new staking scripts and calculate the + // transaction + const stakingInstance = new Staking( + this.network, + stakerBtcInfo, + params, + stakingInput.finalityProviderPksNoCoordHex, + stakingInput.stakingTimelock, + ); + const { + fee, + } = stakingInstance.createStakingExpansionTransaction( + stakingInput.stakingAmountSat, + inputUTXOs, + feeRate, + paramsForPreviousStakingTx, + previousStakingTxInfo, + ); + + return fee; + } + /** * Creates a signed post-staking registration transaction that is ready to be * sent to the Babylon chain. This is used when a staking transaction is @@ -197,7 +386,9 @@ export class BabylonBtcStakingManager { babylonAddress, stakerBtcInfo, params, - this.getInclusionProof(inclusionProof), + { + inclusionProof: this.getInclusionProof(inclusionProof), + }, ); this.ee?.emit("delegation:register", { @@ -333,6 +524,173 @@ export class BabylonBtcStakingManager { return Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); } + /** + * Creates a signed staking expansion transaction that is ready to be sent to + * the BTC network. + * + * @param {StakerInfo} stakerBtcInfo - The staker's BTC information including + * address and public key + * @param {StakingInputs} stakingInput - The staking inputs for the expansion + * @param {Transaction} unsignedStakingExpansionTx - The unsigned staking + * expansion transaction + * @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input + * @param {number} stakingParamsVersion - The version of staking parameters + * that was used when registering the staking expansion delegation. + * @param {Object} previousStakingTxInfo - Information about the previous + * staking transaction + * @param {Array} covenantStakingExpansionSignatures - Covenant committee + * signatures for the expansion + * @returns {Promise} The fully signed staking expansion + * transaction + * @throws {Error} If signing fails, validation fails, or required data is + * missing + */ + async createSignedBtcStakingExpansionTransaction( + stakerBtcInfo: StakerInfo, + stakingInput: StakingInputs, + unsignedStakingExpansionTx: Transaction, + inputUTXOs: UTXO[], + stakingParamsVersion: number, + previousStakingTxInfo: { + stakingTx: Transaction, + paramVersion: number, + stakingInput: StakingInputs, + }, + covenantStakingExpansionSignatures: { + btcPkHex: string; + sigHex: string; + }[], + ): Promise { + validateStakingExpansionInputs( + { + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + }, + ); + + // Get the staking parameters for the current version + // These parameters define the covenant committee and other staking rules + const params = getBabylonParamByVersion( + stakingParamsVersion, + this.stakingParams, + ); + + // Validate that input UTXOs are provided for the funding input + if (inputUTXOs.length === 0) { + throw new Error("No input UTXOs provided"); + } + + // Create a new staking instance with the current parameters + // This will be used to build the PSBT for the expansion transaction + const staking = new Staking( + this.network, + stakerBtcInfo, + params, + stakingInput.finalityProviderPksNoCoordHex, + stakingInput.stakingTimelock, + ); + + const previousParams = getBabylonParamByVersion( + previousStakingTxInfo.paramVersion, + this.stakingParams, + ); + + // Create the PSBT for the staking expansion transaction + // This PSBT will have two inputs: the previous staking output and a + // funding UTXO + const stakingExpansionPsbt = staking.toStakingExpansionPsbt( + unsignedStakingExpansionTx, + inputUTXOs, + previousParams, + previousStakingTxInfo, + ); + + // Define the contract information for the PSBT signing + const contracts: Contract[] = [ + { + id: ContractId.STAKING, + params: { + stakerPk: stakerBtcInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params.covenantNoCoordPks, + covenantThreshold: params.covenantQuorum, + minUnbondingTime: params.unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + }, + }, + ]; + + // Emit an event to notify listeners about the staking expansion + // This can be used for logging, monitoring, or UI updates + this.ee?.emit("delegation:stake", { + stakerPk: stakerBtcInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params.covenantNoCoordPks, + covenantThreshold: params.covenantQuorum, + unbondingTimeBlocks: params.unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + type: "staking", + }); + + // Sign the PSBT using the BTC provider (wallet) + // The wallet will sign the transaction based on the contract information + // provided + const signedStakingPsbtHex = await this.btcProvider.signPsbt( + stakingExpansionPsbt.toHex(), + { + contracts, + action: { + name: ActionName.SIGN_BTC_STAKING_TRANSACTION, + }, + }, + ); + + // Extract the signed transaction from the PSBT + const signedStakingExpansionTx = Psbt.fromHex( + signedStakingPsbtHex, + ).extractTransaction(); + + // Validate that the signed transaction hash matches the unsigned + // transaction hash + // This ensures that the signing process didn't change the transaction + // structure + if ( + signedStakingExpansionTx.getId() !== unsignedStakingExpansionTx.getId() + ) { + throw new Error( + "Staking expansion transaction hash does not match the computed hash", + ); + } + + // Add covenant committee signatures to the transaction + // Convert covenant public keys from hex strings to buffers + // The covenants committee is based on the params at the time of the previous + // staking transaction. Hence using the previous params here. + const covenantBuffers = previousParams.covenantNoCoordPks.map((covenant) => + Buffer.from(covenant, "hex"), + ); + + // Create the witness that includes both the staker's signature and covenant + // signatures + // The witness is the data that proves the transaction is authorized + const witness = createCovenantWitness( + // The first input of the staking expansion transaction is the previous + // staking output. We will attach the covenant signatures to this input + // to unbond the previousstaking output. + signedStakingExpansionTx.ins[0].witness, + covenantBuffers, + covenantStakingExpansionSignatures, + previousParams.covenantQuorum, + ); + + // Overwrite the witness to include the covenant staking expansion signatures + // This makes the transaction valid for submission to the Bitcoin network + signedStakingExpansionTx.ins[0].witness = witness; + + return signedStakingExpansionTx; + } + /** * Creates a partial signed unbonding transaction that is only signed by the * staker. In order to complete the unbonding transaction, the covenant @@ -715,7 +1073,7 @@ export class BabylonBtcStakingManager { * @returns The proof of possession. */ async createProofOfPossession( - channel: "delegation:create" | "delegation:register", + channel: "delegation:create" | "delegation:register" | "delegation:expand", bech32Address: string, stakerBtcAddress: string, ): Promise { @@ -731,13 +1089,31 @@ export class BabylonBtcStakingManager { sigType = BTCSigType.BIP322; } - this.ee?.emit(channel, { + const [chainId, babyTipHeight] = await Promise.all([ + this.babylonProvider.getChainId?.(), + this.babylonProvider.getCurrentHeight?.(), + ]); + + const upgradeConfig = this.upgradeConfig?.pop; + + // Get the message to sign for the proof of possession + const messageToSign = buildPopMessage( bech32Address, + babyTipHeight, + chainId, + upgradeConfig && { + upgradeHeight: upgradeConfig.upgradeHeight, + version: upgradeConfig.version, + }, + ); + + this.ee?.emit(channel, { + messageToSign, type: "proof-of-possession", }); const signedBabylonAddress = await this.btcProvider.signMessage( - bech32Address, + messageToSign, sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa", ); @@ -791,7 +1167,6 @@ export class BabylonBtcStakingManager { /** * Creates a protobuf message for the BTC delegation. - * @param channel - The event channel to emit the message on. * @param stakingInstance - The staking instance. * @param stakingInput - The staking inputs. * @param stakingTx - The staking transaction. @@ -799,19 +1174,32 @@ export class BabylonBtcStakingManager { * @param stakerBtcInfo - The staker's BTC information such as address and * public key * @param params - The staking parameters. - * @param inclusionProof - The inclusion proof of the staking transaction. + * @param options - The options for the BTC delegation. + * @param options.inclusionProof - The inclusion proof of the staking + * transaction. + * @param options.delegationExpansionInfo - The information for the BTC + * delegation expansion. * @returns The protobuf message. */ - public async createBtcDelegationMsg( - channel: "delegation:create" | "delegation:register", + private async createBtcDelegationMsg( + channel: "delegation:create" | "delegation:register" | "delegation:expand", stakingInstance: Staking, stakingInput: StakingInputs, stakingTx: Transaction, bech32Address: string, stakerBtcInfo: StakerInfo, params: StakingParams, - inclusionProof?: btcstaking.InclusionProof, - ) { + options?: { + inclusionProof?: btcstaking.InclusionProof, + delegationExpansionInfo?: { + previousStakingTx: Transaction, + fundingTx: Transaction, + } + } + ): Promise<{ + typeUrl: string, + value: btcstakingtx.MsgCreateBTCDelegation | btcstakingtx.MsgBtcStakeExpand, + }> { if (!params.slashing) { throw new StakingError( StakingErrorCode.INVALID_PARAMS, @@ -957,35 +1345,55 @@ export class BabylonBtcStakingManager { stakerBtcInfo.address, ); - // Prepare the final protobuf message + const commonMsg = { + stakerAddr: bech32Address, + pop: proofOfPossession, + btcPk: Uint8Array.from( + Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex"), + ), + fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => + Uint8Array.from(Buffer.from(pk, "hex")), + ), + stakingTime: stakingInput.stakingTimelock, + stakingValue: stakingInput.stakingAmountSat, + stakingTx: Uint8Array.from(stakingTx.toBuffer()), + slashingTx: Uint8Array.from( + Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), + ), + delegatorSlashingSig: Uint8Array.from(slashingSig), + unbondingTime: params.unbondingTime, + unbondingTx: Uint8Array.from(unbondingTx.toBuffer()), + unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat, + unbondingSlashingTx: Uint8Array.from( + Buffer.from( + clearTxSignatures(signedUnbondingSlashingTx).toHex(), + "hex", + ), + ), + delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), + } + + // If the delegation is an expansion, we use the MsgBtcStakeExpand message + if (options?.delegationExpansionInfo) { + const fundingTx = Uint8Array.from( + options.delegationExpansionInfo.fundingTx.toBuffer()); + const msg = btcstakingtx.MsgBtcStakeExpand.fromPartial({ + ...commonMsg, + previousStakingTxHash: + options.delegationExpansionInfo.previousStakingTx.getId(), + fundingTx, + }); + return { + typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgBtcStakeExpand, + value: msg, + } + } + + // Otherwise, it's a new staking delegation const msg: btcstakingtx.MsgCreateBTCDelegation = btcstakingtx.MsgCreateBTCDelegation.fromPartial({ - stakerAddr: bech32Address, - pop: proofOfPossession, - btcPk: Uint8Array.from( - Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex"), - ), - fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => - Uint8Array.from(Buffer.from(pk, "hex")), - ), - stakingTime: stakingInput.stakingTimelock, - stakingValue: stakingInput.stakingAmountSat, - stakingTx: Uint8Array.from(stakingTx.toBuffer()), - slashingTx: Uint8Array.from( - Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), - ), - delegatorSlashingSig: Uint8Array.from(slashingSig), - unbondingTime: params.unbondingTime, - unbondingTx: Uint8Array.from(unbondingTx.toBuffer()), - unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat, - unbondingSlashingTx: Uint8Array.from( - Buffer.from( - clearTxSignatures(signedUnbondingSlashingTx).toHex(), - "hex", - ), - ), - delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), - stakingTxInclusionProof: inclusionProof, + ...commonMsg, + stakingTxInclusionProof: options?.inclusionProof, }); return { @@ -1021,63 +1429,6 @@ export class BabylonBtcStakingManager { } } -/** - * Extracts the first valid Schnorr signature from a signed transaction. - * - * Since we only handle transactions with a single input and request a signature - * for one public key, there can be at most one signature from the Bitcoin node. - * A valid Schnorr signature is exactly 64 bytes in length. - * - * @param singedTransaction - The signed Bitcoin transaction to extract the signature from - * @returns The first valid 64-byte Schnorr signature found in the transaction witness data, - * or undefined if no valid signature exists - */ -const extractFirstSchnorrSignatureFromTransaction = ( - singedTransaction: Transaction, -): Buffer | undefined => { - // Loop through each input to extract the witness signature - for (const input of singedTransaction.ins) { - if (input.witness && input.witness.length > 0) { - const schnorrSignature = input.witness[0]; - - // Check that it's a 64-byte Schnorr signature - if (schnorrSignature.length === 64) { - return schnorrSignature; // Return the first valid signature found - } - } - } - return undefined; -}; - -/** - * Strips all signatures from a transaction by clearing both the script and - * witness data. This is due to the fact that we only need the raw unsigned - * transaction structure. The signatures are sent in a separate protobuf field - * when creating the delegation message in the Babylon. - * @param tx - The transaction to strip signatures from - * @returns A copy of the transaction with all signatures removed - */ -const clearTxSignatures = (tx: Transaction): Transaction => { - tx.ins.forEach((input) => { - input.script = Buffer.alloc(0); - input.witness = []; - }); - return tx; -}; - -/** - * Derives the merkle proof from the list of hex strings. Note the - * sibling hashes are reversed from hex before concatenation. - * @param merkle - The merkle proof hex strings. - * @returns The merkle proof in hex string format. - */ -const deriveMerkleProof = (merkle: string[]) => { - const proofHex = merkle.reduce((acc: string, m: string) => { - return acc + Buffer.from(m, "hex").reverse().toString("hex"); - }, ""); - return proofHex; -}; - /** * Get the staker signature from the unbonding transaction * This is used mostly for unbonding transactions from phase-1(Observable) diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/observable/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/observable/index.ts index c4cba94a6e..e84d05e220 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/observable/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/observable/index.ts @@ -3,7 +3,8 @@ import { UTXO } from "../../types/UTXO"; import { StakingError, StakingErrorCode } from "../../error"; import { stakingTransaction } from "../transactions"; import { isTaproot } from "../../utils/btc"; -import { toBuffers, validateStakingTxInputData } from "../../utils/staking"; +import { toBuffers } from "../../utils/staking"; +import { validateStakingTxInputData } from "../../utils/staking/validation"; import { TransactionResult } from "../../types/transaction"; import { ObservableStakingScriptData, ObservableStakingScripts } from "./observableStakingScript"; import { StakerInfo, Staking } from ".."; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/psbt.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/psbt.ts index 9e6caf4b05..03d18ee7d1 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/psbt.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/psbt.ts @@ -7,6 +7,8 @@ import { UTXO } from "../types/UTXO"; import { deriveUnbondingOutputInfo } from "../utils/staking"; import { findInputUTXO } from "../utils/utxo/findInputUTXO"; import { getPsbtInputFields } from "../utils/utxo/getPsbtInputFields"; +import { BitcoinScriptType, getScriptType } from "../utils/utxo/getScriptType"; +import { StakingScripts } from "./stakingScript"; /** * Convert a staking transaction to a PSBT. @@ -53,6 +55,145 @@ export const stakingPsbt = ( return psbt; }; +/** + * Convert a staking expansion transaction to a PSBT. + * + * @param {networks.Network} network - The Bitcoin network to use for the PSBT + * @param {Transaction} stakingTx - The staking expansion transaction to convert + * @param {Object} previousStakingTxInfo - Information about the previous staking transaction + * @param {Transaction} previousStakingTxInfo.stakingTx - The previous staking transaction + * @param {number} previousStakingTxInfo.outputIndex - The index of the staking output in the previous transaction + * @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input + * @param {Buffer} [publicKeyNoCoord] - The staker's public key without coordinate (for Taproot) + * @returns {Psbt} The PSBT for the staking expansion transaction + * @throws {Error} If validation fails or required data is missing + */ +export const stakingExpansionPsbt = ( + network: networks.Network, + stakingTx: Transaction, + previousStakingTxInfo: { + stakingTx: Transaction, + outputIndex: number, + }, + inputUTXOs: UTXO[], + previousScripts: StakingScripts, + publicKeyNoCoord?: Buffer, +): Psbt => { + // Initialize PSBT with the specified network + const psbt = new Psbt({ network }); + + // Set transaction version and locktime if provided + if (stakingTx.version !== undefined) psbt.setVersion(stakingTx.version); + if (stakingTx.locktime !== undefined) psbt.setLocktime(stakingTx.locktime); + + // Validate the public key format if provided + if ( + publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH + ) { + throw new Error("Invalid public key"); + } + + // Extract the previous staking output from the previous staking transaction + const previousStakingOutput = previousStakingTxInfo.stakingTx.outs[ + previousStakingTxInfo.outputIndex + ]; + if (!previousStakingOutput) { + throw new Error("Previous staking output not found"); + }; + + // Validate that the previous staking output is a Taproot (P2TR) script + if ( + getScriptType(previousStakingOutput.script) !== BitcoinScriptType.P2TR + ) { + throw new Error("Previous staking output script type is not P2TR"); + } + + // Validate that the staking expansion transaction has exactly 2 inputs + // Input 0: Previous staking output (existing stake) + // Input 1: Funding UTXO (additional funds for fees or staking amount) + if (stakingTx.ins.length !== 2) { + throw new Error( + "Staking expansion transaction must have exactly 2 inputs", + ); + } + + // Validate the first input matches the previous staking transaction + const txInputs = stakingTx.ins; + + // Check that the first input references the correct previous staking + // transaction + if ( + Buffer.from(txInputs[0].hash).reverse().toString("hex") !== previousStakingTxInfo.stakingTx.getId() + ) { + throw new Error("Previous staking input hash does not match"); + } + // Check that the first input references the correct output index + else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) { + throw new Error("Previous staking input index does not match"); + } + + // Build input tapleaf script that spends the previous staking output + const inputScriptTree: Taptree = [ + { output: previousScripts.slashingScript }, + [{ output: previousScripts.unbondingScript }, { output: previousScripts.timelockScript }], + ]; + const inputRedeem = { + output: previousScripts.unbondingScript, + redeemVersion: REDEEM_VERSION, + }; + const p2tr = payments.p2tr({ + internalPubkey, + scriptTree: inputScriptTree, + redeem: inputRedeem, + network, + }); + + if (!p2tr.witness || p2tr.witness.length === 0) { + throw new Error( + "Failed to create P2TR witness for expansion transaction input" + ); + } + + const inputTapLeafScript = { + leafVersion: inputRedeem.redeemVersion, + script: inputRedeem.output, + controlBlock: p2tr.witness[p2tr.witness.length - 1], + }; + + // Add the previous staking input to the PSBT + // This input spends the existing staking output + psbt.addInput({ + hash: txInputs[0].hash, + index: txInputs[0].index, + sequence: txInputs[0].sequence, + witnessUtxo: { + script: previousStakingOutput.script, + value: previousStakingOutput.value, + }, + tapInternalKey: internalPubkey, + tapLeafScript: [inputTapLeafScript], + }); + + // Add the second input (funding UTXO) to the PSBT + // This input provides additional funds for fees or staking amount + const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]); + const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord); + + psbt.addInput({ + hash: txInputs[1].hash, + index: txInputs[1].index, + sequence: txInputs[1].sequence, + ...psbtInputData, + }); + + // Add all outputs from the staking expansion transaction to the PSBT + stakingTx.outs.forEach((o) => { + psbt.addOutput({ script: o.script, value: o.value }); + }); + + return psbt; +}; + export const unbondingPsbt = ( scripts: { unbondingScript: Buffer; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/transactions.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/transactions.ts index 2b70a95418..4031bcac1d 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/transactions.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/transactions.ts @@ -1,4 +1,6 @@ -import { Psbt, Transaction, networks, payments, script, address, opcodes } from "bitcoinjs-lib"; +import { + Psbt, Transaction, networks, payments, script, address, opcodes +} from "bitcoinjs-lib"; import { Taptree } from "bitcoinjs-lib/src/types"; import { BTC_DUST_SAT } from "../constants/dustSat"; @@ -6,9 +8,18 @@ import { internalPubkey } from "../constants/internalPubkey"; import { UTXO } from "../types/UTXO"; import { PsbtResult, TransactionResult } from "../types/transaction"; import { isValidBitcoinAddress, transactionIdToHash } from "../utils/btc"; -import { getStakingTxInputUTXOsAndFees, getWithdrawTxFee } from "../utils/fee"; +import { + getStakingExpansionTxFundingUTXOAndFees, + getStakingTxInputUTXOsAndFees, + getWithdrawTxFee, +} from "../utils/fee"; import { inputValueSum } from "../utils/fee/utils"; -import { buildStakingTransactionOutputs, deriveUnbondingOutputInfo } from "../utils/staking"; +import { + buildStakingTransactionOutputs, + deriveStakingOutputInfo, + deriveUnbondingOutputInfo, + findMatchingTxOutputIndex, +} from "../utils/staking"; import { NON_RBF_SEQUENCE, TRANSACTION_VERSION } from "../constants/psbt"; import { CovenantSignature } from "../types/covenantSignatures"; import { REDEEM_VERSION } from "../constants/transaction"; @@ -125,6 +136,144 @@ export function stakingTransaction( }; } +/** + * Expand an existing staking transaction with additional finality providers + * or renew timelock. + * + * This function builds a Bitcoin transaction that: + * 1. Spends the previous staking transaction output as the first input + * 2. Uses a funding UTXO as the second input to cover transaction fees + * 3. Creates new staking outputs where the timelock is renewed or FPs added + * 4. Returns any remaining funds as change + * + * @param network - Bitcoin network (mainnet, testnet, etc.) + * @param scripts - Scripts for the new staking outputs + * @param amount - Total staking amount (must equal previous staking amount, + * we only support equal amounts for now) + * @param changeAddress - Bitcoin address to receive change from funding UTXO + * @param feeRate - Fee rate in satoshis per byte + * @param inputUTXOs - Available UTXOs to use for funding the expansion + * @param previousStakingTxInfo - Details of the previous staking transaction + * being expanded + * @returns {TransactionResult & { fundingUTXO: UTXO }} containing the built + * transaction and calculated fee, and the funding UTXO + */ +export function stakingExpansionTransaction( + network: networks.Network, + scripts: { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; + }, + amount: number, + changeAddress: string, + feeRate: number, + inputUTXOs: UTXO[], + previousStakingTxInfo: { + stakingTx: Transaction, + scripts: { + timelockScript: Buffer; + unbondingScript: Buffer; + slashingScript: Buffer; + }, + }, +): TransactionResult & { + fundingUTXO: UTXO; +} { + // Validate input parameters + if (amount <= 0 || feeRate <= 0) { + throw new Error("Amount and fee rate must be bigger than 0"); + } else if (!isValidBitcoinAddress(changeAddress, network)) { + throw new Error("Invalid BTC change address"); + } + + // Derive the output address and amount from the previous staking transaction + // scripts. This helps us locate the specific output in the previous + // transaction + const previousStakingOutputInfo = deriveStakingOutputInfo( + previousStakingTxInfo.scripts, network + ); + + // Find the output index of the previous staking transaction in the + // transaction outputs. This method will throw an error if the output + // is not found. + const previousStakingOutputIndex = findMatchingTxOutputIndex( + previousStakingTxInfo.stakingTx, + previousStakingOutputInfo.outputAddress, + network + ); + + // Extract the actual staking amount from the previous transaction output + const previousStakingAmount = previousStakingTxInfo.stakingTx.outs[ + previousStakingOutputIndex + ].value; + + // Validate that the expansion amount matches the previous staking amount + // According to Babylon protocol, expansion amount must be >= previous amount + // Currently, this library only supports equal amounts (no stake increase) + if (amount !== previousStakingAmount) { + throw new Error( + "Expansion staking transaction amount must be equal to the previous " + + "staking amount. Increase of the staking amount is not supported yet.", + ); + } + + // Build the staking outputs for the expansion transaction + // These outputs will contain the new scripts with expanded timelock or FPs + const stakingOutputs = buildStakingTransactionOutputs( + scripts, network, amount, + ); + + // Select a single funding UTXO and calculate the required fee + // The funding UTXO will be used as the second input to cover transaction fees + const { selectedUTXO, fee } = getStakingExpansionTxFundingUTXOAndFees( + inputUTXOs, + feeRate, + stakingOutputs, + ); + + // Initialize the transaction with the standard version + const tx = new Transaction(); + tx.version = TRANSACTION_VERSION; + + // Add the first input: previous staking transaction output + // This is the existing stake that we're expanding + tx.addInput( + previousStakingTxInfo.stakingTx.getHash(), + previousStakingOutputIndex, + NON_RBF_SEQUENCE, + ); + + // Add the second input: selected funding UTXO + // This provides the funds to cover transaction fees + tx.addInput( + transactionIdToHash(selectedUTXO.txid), + selectedUTXO.vout, + NON_RBF_SEQUENCE, + ); + + // Add all staking outputs to the transaction + // These represent the expanded stake with new finality provider coverage + stakingOutputs.forEach((o) => { + tx.addOutput(o.scriptPubKey, o.value); + }); + + // Add a change output if there are remaining funds from the funding UTXO + // Only create change if the remaining amount is above the dust threshold + if (selectedUTXO.value - fee > BTC_DUST_SAT) { + tx.addOutput( + address.toOutputScript(changeAddress, network), + selectedUTXO.value - fee, + ); + } + + return { + transaction: tx, + fee, + fundingUTXO: selectedUTXO, + }; +} + /** * Constructs a withdrawal transaction for manually unbonded delegation. * @@ -703,15 +852,19 @@ export const createCovenantWitness = ( + `got: ${covenantSigs.length}` ); } - // Verify all btcPkHex from covenantSigs exist in paramsCovenants - for (const sig of covenantSigs) { + // Filter out the signatures that are not in the params covenants + const filteredCovenantSigs = covenantSigs.filter((sig) => { const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex"); - if (!paramsCovenants.some(covenant => covenant.equals(btcPkHexBuf))) { - throw new Error( - `Covenant signature public key ${sig.btcPkHex} not found in params covenants` - ); - } + return paramsCovenants.some(covenant => covenant.equals(btcPkHexBuf)); + }); + + if (filteredCovenantSigs.length < covenantQuorum) { + throw new Error( + `Not enough valid covenant signatures. Required: ${covenantQuorum}, ` + + `got: ${filteredCovenantSigs.length}` + ); } + // We only take exactly covenantQuorum number of signatures, even if more are provided. // Including extra signatures will cause the unbonding transaction to fail validation. // This is because the witness script expects exactly covenantQuorum number of signatures diff --git a/modules/babylonlabs-io-btc-staking-ts/src/types/events.ts b/modules/babylonlabs-io-btc-staking-ts/src/types/events.ts index af3ddf25b5..81c1a643af 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/types/events.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/types/events.ts @@ -15,6 +15,7 @@ export interface ManagerEvents { "delegation:stake": (data?: EventData) => void; "delegation:unbond": (data?: EventData) => void; "delegation:withdraw": (data?: EventData) => void; + "delegation:expand": (data?: EventData) => void; } export type DelegationEvent = keyof ManagerEvents; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/types/manager.ts b/modules/babylonlabs-io-btc-staking-ts/src/types/manager.ts index cf83ccbc03..bf3aeae7a5 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/types/manager.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/types/manager.ts @@ -22,6 +22,9 @@ export interface BtcProvider { message: string, type: "ecdsa" | "bip322-simple", ) => Promise; + + // Get the transaction hex from the transaction ID + getTransactionHex(txid: string): Promise; } export interface BabylonProvider { @@ -39,6 +42,20 @@ export interface BabylonProvider { typeUrl: string; value: T; }) => Promise; + + /** + * Gets the current height of the Babylon Genesis chain. + * + * @returns {Promise} The current Babylon chain height + */ + getCurrentHeight?: () => Promise; + + /** + * Gets the chain ID of the Babylon Genesis chain. + * + * @returns {Promise} The Babylon chain ID + */ + getChainId?: () => Promise; } export interface StakingInputs { @@ -61,3 +78,31 @@ export interface InclusionProof { // The block hash of the block that contains the transaction blockHashHex: string; } + +/** + * Upgrade configuration for Babylon POP (Proof of Possession) context. + * This is used to determine when to switch to the new POP context format + * based on the Babylon chain height and version. + */ +export interface UpgradeConfig { + /** + * POP context upgrade configuration. + */ + pop?: PopUpgradeConfig; +} + +/** + * Configuration for POP context upgrade. + * - upgradeHeight: The Babylon chain height at which the POP context upgrade is activated. + * - version: The version of the POP context to use after the upgrade. + */ +export interface PopUpgradeConfig { + /** + * The Babylon chain height at which the POP context upgrade is activated. + */ + upgradeHeight: number; + /** + * The version of the POP context to use after the upgrade. + */ + version: number; +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/src/utils/fee/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/utils/fee/index.ts index 4f53a73785..30a6c160f8 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/utils/fee/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/utils/fee/index.ts @@ -6,6 +6,7 @@ import { OP_RETURN_OUTPUT_VALUE_SIZE, OP_RETURN_VALUE_SERIALIZE_SIZE, P2TR_INPUT_SIZE, + P2TR_STAKING_EXPANSION_INPUT_SIZE, TX_BUFFER_SIZE_OVERHEAD, WALLET_RELAY_FEE_RATE_THRESHOLD, WITHDRAW_TX_BUFFER_SIZE, @@ -94,6 +95,99 @@ export const getStakingTxInputUTXOsAndFees = ( }; }; +/** + * Calculates the required funding UTXO and fees for a staking expansion transaction. + * + * This function selects a single UTXO from available UTXOs to cover: + * 1. Transaction fees for the expansion + * 2. Any additional staking amount beyond the previous stake + * + * @param availableUTXOs - List of available UTXOs to choose from for funding + * @param previousStakingTx - Details of the previous staking transaction being expanded + * @param stakingAmount - Total staking amount for the expansion (includes previous + additional) + * @param feeRate - Fee rate in satoshis per byte + * @param outputs - Transaction outputs for the expansion + * @returns Object containing the selected funding UTXO and calculated fee + */ +export const getStakingExpansionTxFundingUTXOAndFees = ( + availableUTXOs: UTXO[], + feeRate: number, + outputs: TransactionOutput[], +): { + selectedUTXO: UTXO; + fee: number; +} => { + // Validate that we have UTXOs to work with + if (availableUTXOs.length === 0) { + throw new Error("Insufficient funds"); + } + + // Filter out invalid UTXOs by checking if their script can be decompiled + // This ensures we only work with properly formatted Bitcoin scripts + const validUTXOs = availableUTXOs.filter((utxo) => { + const script = Buffer.from(utxo.scriptPubKey, "hex"); + const decompiledScript = bitcoinScript.decompile(script); + return decompiledScript && decompiledScript.length > 0; + }); + + if (validUTXOs.length === 0) { + throw new Error("Insufficient funds: no valid UTXOs available for staking"); + } + + // Sort available UTXOs from lowest to highest value for optimal selection + // This helps us avoid selecting large UTXOs which can be used + // for other activities. + const sortedUTXOs = validUTXOs.sort((a, b) => a.value - b.value); + + // Iterate through UTXOs to find one that can cover the required fees + for (const utxo of sortedUTXOs) { + // Calculate the estimated transaction size including: + // - Base transaction size (additional UTXOs + Outputs) + // - Previous staking transaction output as the input for the expansion tx + // Note: Staking transactions use P2TR (Taproot) format, + // hence P2TR_STAKING_EXPANSION_INPUT_SIZE accounts for the witness size + // including covenant signatures and is calibrated for a typical covenant + // quorum of 6 signatures. + const estimatedSize = getEstimatedSize( + [utxo], + outputs, + ) + P2TR_STAKING_EXPANSION_INPUT_SIZE; + + // Calculate base fee: size * rate + buffer fee for network congestion + let estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate); + + // Check if this UTXO has enough value to cover the estimated fee + // We are selecting a UTXO that can only cover the fee as + // in the case of stake expansion we only want the additional UTXO to cover + // the staking fee. + // TODO: In the future, we will want to support selecting a UTXO for an increased + // staking amount. + if (utxo.value >= estimatedFee) { + // Check if there will be change left after paying the fee + // If change amount is above dust threshold, we need to add a change output + // which increases the transaction size and fee + if (utxo.value - estimatedFee > BTC_DUST_SAT) { + // Add fee for the change output + estimatedFee += getEstimatedChangeOutputSize() * feeRate; + } + // Finally, ensure the estimated fee is not greater than the UTXO value + if (utxo.value >= estimatedFee) { + return { + selectedUTXO: utxo, + fee: estimatedFee, + }; + } + // If the UTXO value is less than the estimated fee, we need to continue + // searching for a UTXO that can cover the fees. + } + } + + // If no UTXO can cover the fees, throw an error + throw new Error( + "Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction.", + ); +}; + /** * Calculates the estimated fee for a withdrawal transaction. diff --git a/modules/babylonlabs-io-btc-staking-ts/src/utils/pop.ts b/modules/babylonlabs-io-btc-staking-ts/src/utils/pop.ts new file mode 100644 index 0000000000..0bfc789eb0 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/src/utils/pop.ts @@ -0,0 +1,59 @@ +import { sha256 } from "bitcoinjs-lib/src/crypto"; + +import { STAKING_MODULE_ADDRESS } from "../constants/staking"; + +/** + * Creates the context string for the staker POP following RFC-036. + * See: https://github.com/babylonlabs-io/pm/blob/main/rfc/rfc-036-replay-attack-protection.md + * @param chainId - The Babylon chain ID + * @param popContextVersion - The POP context version (defaults to 0) + * @returns The hex encoded SHA-256 hash of the context string. + */ +export function createStakerPopContext( + chainId: string, + popContextVersion: number = 0, +): string { + // Context string format following RFC-036: + // Format: btcstaking/{version}/{operation_type}/{chain_id}/{module_address} + // + // Fields: + // - btcstaking: Protocol identifier for Bitcoin staking operations + // - version: POP context version (integer, defaults to 0) + // - operation_type: Type of operation ("staker_pop" for staker proof of possession) + // - chain_id: The Babylon chain ID for domain separation + // - module_address: The staking module address for additional context + const contextString = `btcstaking/${popContextVersion}/staker_pop/${chainId}/${STAKING_MODULE_ADDRESS}`; + return sha256(Buffer.from(contextString, "utf8")).toString("hex"); +} + +/** + * Creates the POP message to sign based on upgrade configuration and current height. + * RFC-036: If the Babylon tip height is greater than or equal to the POP context + * upgrade height, use the new context format, otherwise use legacy format. + * @param currentHeight - The current Babylon tip height + * @param bech32Address - The staker's bech32 address + * @param chainId - The Babylon chain ID + * @param upgradeConfig - Optional upgrade configuration with height and version + * @returns The message to sign (either just the address or context hash + address) + */ +export function buildPopMessage( + bech32Address: string, + currentHeight?: number, + chainId?: string, + upgradeConfig?: { upgradeHeight: number; version: number }, +): string { + // RFC-036: If upgrade is configured and current height >= upgrade height, use new context format + // https://github.com/babylonlabs-io/pm/blob/main/rfc/rfc-036-replay-attack-protection.md + if ( + chainId !== undefined && + upgradeConfig?.upgradeHeight !== undefined && + upgradeConfig.version !== undefined && + currentHeight !== undefined && + currentHeight >= upgradeConfig.upgradeHeight + ) { + const contextHash = createStakerPopContext(chainId, upgradeConfig.version); + return contextHash + bech32Address; + } + + return bech32Address; +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/index.ts index d4e0034110..e86d3740d2 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/index.ts @@ -1,13 +1,8 @@ import { address, networks, payments, Transaction } from "bitcoinjs-lib"; import { Taptree } from "bitcoinjs-lib/src/types"; import { internalPubkey } from "../../constants/internalPubkey"; -import { MIN_UNBONDING_OUTPUT_VALUE } from "../../constants/unbonding"; import { StakingError, StakingErrorCode } from "../../error"; -import { StakingParams } from "../../types/params"; import { TransactionOutput } from "../../types/psbtOutputs"; -import { UTXO } from "../../types/UTXO"; -import { isValidNoCoordPublicKey } from "../btc"; - export interface OutputInfo { scriptPubKey: Buffer; outputAddress: string; @@ -204,190 +199,78 @@ export const findMatchingTxOutputIndex = ( }; /** - * Validate the staking transaction input data. + * toBuffers converts an array of strings to an array of buffers. * - * @param {number} stakingAmountSat - The staking amount in satoshis. - * @param {number} timelock - The staking time in blocks. - * @param {StakingParams} params - The staking parameters. - * @param {UTXO[]} inputUTXOs - The input UTXOs. - * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte - * @throws {StakingError} - If the input data is invalid. + * @param {string[]} inputs - The input strings. + * @returns {Buffer[]} - The buffers. + * @throws {StakingError} - If the values cannot be converted to buffers. */ -export const validateStakingTxInputData = ( - stakingAmountSat: number, - timelock: number, - params: StakingParams, - inputUTXOs: UTXO[], - feeRate: number, -) => { - if ( - stakingAmountSat < params.minStakingAmountSat || - stakingAmountSat > params.maxStakingAmountSat - ) { - throw new StakingError( - StakingErrorCode.INVALID_INPUT, - "Invalid staking amount", - ); - } - - if ( - timelock < params.minStakingTimeBlocks || - timelock > params.maxStakingTimeBlocks - ) { - throw new StakingError(StakingErrorCode.INVALID_INPUT, "Invalid timelock"); - } - - if (inputUTXOs.length == 0) { - throw new StakingError( +export const toBuffers = (inputs: string[]): Buffer[] => { + try { + return inputs.map((i) => Buffer.from(i, "hex")); + } catch (error) { + throw StakingError.fromUnknown( + error, StakingErrorCode.INVALID_INPUT, - "No input UTXOs provided", + "Cannot convert values to buffers", ); } - if (feeRate <= 0) { - throw new StakingError(StakingErrorCode.INVALID_INPUT, "Invalid fee rate"); - } }; + /** - * Validate the staking parameters. - * Extend this method to add additional validation for staking parameters based - * on the staking type. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the parameters are invalid. + * Strips all signatures from a transaction by clearing both the script and + * witness data. This is due to the fact that we only need the raw unsigned + * transaction structure. The signatures are sent in a separate protobuf field + * when creating the delegation message in the Babylon. + * @param tx - The transaction to strip signatures from + * @returns A copy of the transaction with all signatures removed */ -export const validateParams = (params: StakingParams) => { - // Check covenant public keys - if (params.covenantNoCoordPks.length == 0) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Could not find any covenant public keys", - ); - } - if (params.covenantNoCoordPks.length < params.covenantQuorum) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Covenant public keys must be greater than or equal to the quorum", - ); - } - params.covenantNoCoordPks.forEach((pk) => { - if (!isValidNoCoordPublicKey(pk)) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Covenant public key should contains no coordinate", - ); - } +export const clearTxSignatures = (tx: Transaction): Transaction => { + tx.ins.forEach((input) => { + input.script = Buffer.alloc(0); + input.witness = []; }); - // Check other parameters - if (params.unbondingTime <= 0) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Unbonding time must be greater than 0", - ); - } - if (params.unbondingFeeSat <= 0) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Unbonding fee must be greater than 0", - ); - } - if (params.maxStakingAmountSat < params.minStakingAmountSat) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Max staking amount must be greater or equal to min staking amount", - ); - } - if ( - params.minStakingAmountSat < - params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE - ) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - `Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`, - ); - } - if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Max staking time must be greater or equal to min staking time", - ); - } - if (params.minStakingTimeBlocks <= 0) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "Min staking time must be greater than 0", - ); - } - if (params.covenantQuorum <= 0) { - throw new StakingError( - StakingErrorCode.INVALID_PARAMS, - "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.slashingPkScriptHex.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", - ); - } - } + return tx; }; /** - * Validate the staking timelock. - * - * @param {number} stakingTimelock - The staking timelock. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the staking timelock is invalid. + * Derives the merkle proof from the list of hex strings. Note the + * sibling hashes are reversed from hex before concatenation. + * @param merkle - The merkle proof hex strings. + * @returns The merkle proof in hex string format. */ -export const validateStakingTimelock = ( - stakingTimelock: number, - params: StakingParams, -) => { - if ( - stakingTimelock < params.minStakingTimeBlocks || - stakingTimelock > params.maxStakingTimeBlocks - ) { - throw new StakingError( - StakingErrorCode.INVALID_INPUT, - "Staking transaction timelock is out of range", - ); - } +export const deriveMerkleProof = (merkle: string[]) => { + const proofHex = merkle.reduce((acc: string, m: string) => { + return acc + Buffer.from(m, "hex").reverse().toString("hex"); + }, ""); + return proofHex; }; /** - * toBuffers converts an array of strings to an array of buffers. + * Extracts the first valid Schnorr signature from a signed transaction. * - * @param {string[]} inputs - The input strings. - * @returns {Buffer[]} - The buffers. - * @throws {StakingError} - If the values cannot be converted to buffers. + * Since we only handle transactions with a single input and request a signature + * for one public key, there can be at most one signature from the Bitcoin node. + * A valid Schnorr signature is exactly 64 bytes in length. + * + * @param singedTransaction - The signed Bitcoin transaction to extract the signature from + * @returns The first valid 64-byte Schnorr signature found in the transaction witness data, + * or undefined if no valid signature exists */ -export const toBuffers = (inputs: string[]): Buffer[] => { - try { - return inputs.map((i) => Buffer.from(i, "hex")); - } catch (error) { - throw StakingError.fromUnknown( - error, - StakingErrorCode.INVALID_INPUT, - "Cannot convert values to buffers", - ); +export const extractFirstSchnorrSignatureFromTransaction = ( + singedTransaction: Transaction, +): Buffer | undefined => { + // Loop through each input to extract the witness signature + for (const input of singedTransaction.ins) { + if (input.witness && input.witness.length > 0) { + const schnorrSignature = input.witness[0]; + + // Check that it's a 64-byte Schnorr signature + if (schnorrSignature.length === 64) { + return schnorrSignature; // Return the first valid signature found + } + } } + return undefined; }; diff --git a/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/validation.ts b/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/validation.ts new file mode 100644 index 0000000000..73d0dcc061 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/src/utils/staking/validation.ts @@ -0,0 +1,280 @@ +import { MIN_UNBONDING_OUTPUT_VALUE } from "../../constants/unbonding"; +import { StakingError, StakingErrorCode } from "../../error"; +import { StakingInputs, StakingParams, UTXO } from "../../types"; +import { isValidBabylonAddress } from "../babylon"; +import { isValidNoCoordPublicKey } from "../btc"; + +/** + * Validates the staking expansion input + * @param babylonBtcTipHeight - The Babylon BTC tip height + * @param inputUTXOs - The input UTXOs + * @param stakingInput - The staking input + * @param previousStakingInput - The previous staking input + * @param babylonAddress - The Babylon address + * @returns true if validation passes, throws error if validation fails + */ +export const validateStakingExpansionInputs = ( + { + babylonBtcTipHeight, + inputUTXOs, + stakingInput, + previousStakingInput, + babylonAddress, + }: { + babylonBtcTipHeight?: number, + inputUTXOs: UTXO[], + stakingInput: StakingInputs, + previousStakingInput: StakingInputs, + babylonAddress?: string, + } +) => { + if (babylonBtcTipHeight === 0) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "Babylon BTC tip height cannot be 0", + ); + } + if (!inputUTXOs || inputUTXOs.length === 0) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "No input UTXOs provided", + ); + } + if (babylonAddress && !isValidBabylonAddress(babylonAddress)) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "Invalid Babylon address", + ); + } + + // TODO: We currently don't support increasing the staking amount + if (stakingInput.stakingAmountSat !== previousStakingInput.stakingAmountSat) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "Staking expansion amount must equal the previous staking amount", + ); + } + // Check the previous staking transaction's finality providers + // are a subset of the new staking input's finality providers + const currentFPs = stakingInput.finalityProviderPksNoCoordHex; + const previousFPs = previousStakingInput.finalityProviderPksNoCoordHex; + + // Check if all previous finality providers are included in the current + // staking + const missingPreviousFPs = previousFPs.filter(prevFp => !currentFPs.includes(prevFp)); + + if (missingPreviousFPs.length > 0) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + `Invalid staking expansion: all finality providers from the previous + staking must be included. Missing: ${missingPreviousFPs.join(", ")}`, + ); + } +} + +/** + * Validate the staking transaction input data. + * + * @param {number} stakingAmountSat - The staking amount in satoshis. + * @param {number} timelock - The staking time in blocks. + * @param {StakingParams} params - The staking parameters. + * @param {UTXO[]} inputUTXOs - The input UTXOs. + * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte + * @throws {StakingError} - If the input data is invalid. + */ +export const validateStakingTxInputData = ( + stakingAmountSat: number, + timelock: number, + params: StakingParams, + inputUTXOs: UTXO[], + feeRate: number, +) => { + if ( + stakingAmountSat < params.minStakingAmountSat || + stakingAmountSat > params.maxStakingAmountSat + ) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "Invalid staking amount", + ); + } + + if ( + timelock < params.minStakingTimeBlocks || + timelock > params.maxStakingTimeBlocks + ) { + throw new StakingError(StakingErrorCode.INVALID_INPUT, "Invalid timelock"); + } + + if (inputUTXOs.length == 0) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "No input UTXOs provided", + ); + } + if (feeRate <= 0) { + throw new StakingError(StakingErrorCode.INVALID_INPUT, "Invalid fee rate"); + } +}; + +/** + * Validate the staking parameters. + * Extend this method to add additional validation for staking parameters based + * on the staking type. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the parameters are invalid. + */ +export const validateParams = (params: StakingParams) => { + // Check covenant public keys + if (params.covenantNoCoordPks.length == 0) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Could not find any covenant public keys", + ); + } + if (params.covenantNoCoordPks.length < params.covenantQuorum) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Covenant public keys must be greater than or equal to the quorum", + ); + } + params.covenantNoCoordPks.forEach((pk) => { + if (!isValidNoCoordPublicKey(pk)) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Covenant public key should contains no coordinate", + ); + } + }); + // Check other parameters + if (params.unbondingTime <= 0) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Unbonding time must be greater than 0", + ); + } + if (params.unbondingFeeSat <= 0) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Unbonding fee must be greater than 0", + ); + } + if (params.maxStakingAmountSat < params.minStakingAmountSat) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Max staking amount must be greater or equal to min staking amount", + ); + } + if ( + params.minStakingAmountSat < + params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE + ) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + `Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`, + ); + } + if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Max staking time must be greater or equal to min staking time", + ); + } + if (params.minStakingTimeBlocks <= 0) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "Min staking time must be greater than 0", + ); + } + if (params.covenantQuorum <= 0) { + throw new StakingError( + StakingErrorCode.INVALID_PARAMS, + "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.slashingPkScriptHex.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", + ); + } + } +}; + +/** + * Validate the staking timelock. + * + * @param {number} stakingTimelock - The staking timelock. + * @param {StakingParams} params - The staking parameters. + * @throws {StakingError} - If the staking timelock is invalid. + */ +export const validateStakingTimelock = ( + stakingTimelock: number, + params: StakingParams, +) => { + if ( + stakingTimelock < params.minStakingTimeBlocks || + stakingTimelock > params.maxStakingTimeBlocks + ) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + "Staking transaction timelock is out of range", + ); + } +}; + +/** + * Validate the staking expansion covenant quorum. + * + * The quorum is the number of covenant members that must be active in the + * previous staking transaction in order to expand the staking. + * + * If the quorum is not met, the staking expansion will fail. + * + * @param {StakingParams} paramsForPreviousStakingTx - The staking parameters + * for the previous staking transaction. + * @param {StakingParams} paramsForCurrentStakingTx - The staking parameters + * for the current staking transaction. + * @throws {StakingError} - If the staking expansion covenant quorum is invalid. + */ +export const validateStakingExpansionCovenantQuorum = ( + paramsForPreviousStakingTx: StakingParams, + paramsForCurrentStakingTx: StakingParams, +) => { + const previousCovenantMembers = paramsForPreviousStakingTx.covenantNoCoordPks; + const currentCovenantMembers = paramsForCurrentStakingTx.covenantNoCoordPks; + const requiredQuorum = paramsForPreviousStakingTx.covenantQuorum; + + // Count how many previous covenant members are still active + const activePreviousMembers = previousCovenantMembers.filter( + prevMember => currentCovenantMembers.includes(prevMember) + ).length; + + if (activePreviousMembers < requiredQuorum) { + throw new StakingError( + StakingErrorCode.INVALID_INPUT, + `Staking expansion failed: insufficient covenant quorum. ` + + `Required: ${requiredQuorum}, Available: ${activePreviousMembers}. ` + + `Too many covenant members have rotated out.` + ); + } +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts new file mode 100644 index 0000000000..2df6136235 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts @@ -0,0 +1,488 @@ +import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; +import * as bitcoin from "bitcoinjs-lib"; +import ECPairFactory from "ecpair"; +import { + slashEarlyUnbondedTransaction, + slashTimelockUnbondedTransaction, + unbondingTransaction, +} from "../../../src"; +import { Staking } from "../../../src/staking"; +import { UTXO } from "../../../src/types/UTXO"; +import { StakingParams } from "../../../src/types/params"; +import { generateRandomAmountSlices } from "../math"; +import { StakingScriptData, StakingScripts } from "../../../src/index"; +import { MIN_UNBONDING_OUTPUT_VALUE } from "../../../src/constants/unbonding"; +import { payments, Psbt, Transaction } from "bitcoinjs-lib"; +import { TRANSACTION_VERSION } from "../../../src/constants/psbt"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { internalPubkey } from "../../../src/constants/internalPubkey"; + +bitcoin.initEccLib(ecc); +const ECPair = ECPairFactory(ecc); + +export const DEFAULT_TEST_FEE_RATE = 15; + +export interface KeyPair { + privateKey: string; + publicKey: string; + publicKeyNoCoord: string; + keyPair: bitcoin.Signer; +} + +export type SlashingType = "earlyUnbonded" | "timelockExpire"; + +export class StakingDataGenerator { + network: bitcoin.networks.Network; + + constructor(network: bitcoin.networks.Network) { + this.network = network; + } + + generateStakingParams( + fixedTerm: boolean = false, committeeSize?: number, + minStakingAmount?: number + ): StakingParams { + if (!committeeSize) { + committeeSize = this.getRandomIntegerBetween(5, 50); + } + const covenantNoCoordPks = this.generateRandomCovenantCommittee(committeeSize).map( + (buffer) => buffer.toString("hex"), + ); + const covenantQuorum = Math.floor(committeeSize/2) + 1; + if (minStakingAmount && minStakingAmount < MIN_UNBONDING_OUTPUT_VALUE + 1) { + throw new Error("Minimum staking amount is less than the unbonding output value"); + } + const minStakingAmountSat = minStakingAmount ? minStakingAmount : this.getRandomIntegerBetween(100000, 1000000000); + const minStakingTimeBlocks = this.getRandomIntegerBetween(1, 2000); + 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, + unbondingTime, + unbondingFeeSat: minStakingAmountSat - MIN_UNBONDING_OUTPUT_VALUE - 1, + minStakingAmountSat, + maxStakingAmountSat: this.getRandomIntegerBetween( + minStakingAmountSat, minStakingAmountSat + 1000000000, + ), + minStakingTimeBlocks, + maxStakingTimeBlocks, + slashing: { + slashingRate, + slashingPkScriptHex: getRandomPaymentScriptHex(this.generateRandomKeyPair().publicKey), + minSlashingTxFeeSat, + } + }; + } + + generateMockStakingScripts( + stakerKeyPair?: KeyPair, + ): StakingScripts { + if (!stakerKeyPair) { + stakerKeyPair = this.generateRandomKeyPair(); + } + const committeeSize = this.getRandomIntegerBetween(1, 10); + const globalParams = this.generateStakingParams( + false, + committeeSize, + ); + const stakingTxTimelock = this.generateRandomTimelock(globalParams); + + return this.generateStakingScriptData( + stakerKeyPair.publicKeyNoCoord, + globalParams, + stakingTxTimelock, + ); + } + + generateStakingScriptData ( + stakerPkNoCoord: string, + params: StakingParams, + timelock: number, + ): StakingScripts { + const fpPkHex = this.generateRandomKeyPair().publicKeyNoCoord; + return new StakingScriptData( + Buffer.from(stakerPkNoCoord, "hex"), + [Buffer.from(fpPkHex, "hex")], + params.covenantNoCoordPks.map((pk: string) => Buffer.from(pk, "hex")), + params.covenantQuorum, + timelock, + params.unbondingTime, + ).buildScripts(); + } + + generateRandomTxId = () => { + const randomBuffer = Buffer.alloc(32); + for (let i = 0; i < 32; i++) { + randomBuffer[i] = Math.floor(Math.random() * 256); + } + return randomBuffer.toString("hex"); + }; + + generateRandomKeyPair = () => { + const keyPair = ECPair.makeRandom({ network: this.network }); + const { privateKey, publicKey } = keyPair; + if (!privateKey || !publicKey) { + throw new Error("Failed to generate random key pair"); + } + const pk = publicKey.toString("hex"); + + return { + privateKey: privateKey.toString("hex"), + publicKey: pk, + publicKeyNoCoord: pk.slice(2), + keyPair, + }; + }; + + // Generate a random timelock value + // ranged from 1 to 65535 + generateRandomTimelock = ( + params: { minStakingTimeBlocks: number, maxStakingTimeBlocks: number}, + ) => { + if (params.minStakingTimeBlocks === params.maxStakingTimeBlocks) { + return params.minStakingTimeBlocks; + } + return this.getRandomIntegerBetween( + params.minStakingTimeBlocks, + params.maxStakingTimeBlocks, + ); + }; + + generateRandomUnbondingTime = (timelock: number) => { + return Math.floor(Math.random() * timelock) + 1; + }; + + generateRandomFeeRates = () => { + return Math.floor(Math.random() * 1000) + 1; + }; + + // Real values will likely be in range 0.0001 to 0.3 + generateRandomSlashingRate(min: number = 0.0001, max: number = 0.3): number { + return parseFloat((Math.random() * (max - min) + min).toFixed(4)); + } + + // Convenant committee are a list of public keys that are used to sign a covenant + generateRandomCovenantCommittee = (size: number): Buffer[] => { + const committe: Buffer[] = []; + for (let i = 0; i < size; i++) { + const publicKeyNoCoord = this.generateRandomKeyPair().publicKeyNoCoord; + committe.push(Buffer.from(publicKeyNoCoord, "hex")); + } + return committe; + }; + + + getAddressAndScriptPubKey = (publicKey: string) => { + return { + taproot: this.getTaprootAddress(publicKey), + nativeSegwit: this.getNativeSegwitAddress(publicKey), + }; + }; + + getNetwork = () => { + return this.network; + }; + + generateRandomUTXOs = ( + balance: number, + numberOfUTXOs: number, + scriptPubKey?: string, + ): UTXO[] => { + if (!scriptPubKey) { + const pk = this.generateRandomKeyPair().publicKey; + const { nativeSegwit } = this.getAddressAndScriptPubKey(pk); + scriptPubKey = nativeSegwit.scriptPubKey; + } + const slices = generateRandomAmountSlices(balance, numberOfUTXOs); + return slices.map((v) => { + return { + txid: this.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: scriptPubKey, + value: v, + }; + }); + }; + + /** + * Generates a random integer between min and max. + * + * @param {number} min - The minimum number. + * @param {number} max - The maximum number. + * @returns {number} - A random integer between min and max. + */ + getRandomIntegerBetween = (min: number, max: number): number => { + if (min > max) { + throw new Error( + "The minimum number should be less than or equal to the maximum number.", + ); + } + return Math.floor(Math.random() * (max - min + 1)) + min; + }; + + /** + * The main entry point for generating a random staking transaction and + * its instance, as well as getting the staker info, params, and staking amount + * etc + * @param network - The network to use + * @param feeRate - The fee rate to use + * @param stakerKeyPair - The staker key pair to use + * @param stakingAmount - The staking amount to use + * @param addressType - The address type to use + * @param params - The staking parameters to use + * @returns {Object} - A random staking transaction + */ + generateRandomStakingTransaction = ( + network: bitcoin.networks.Network, + feeRate: number = DEFAULT_TEST_FEE_RATE, + stakerKeyPair?: KeyPair, + stakingAmount?: number, + addressType?: "taproot" | "nativeSegwit", + params?: StakingParams, + ) => { + if (!stakerKeyPair) { + stakerKeyPair = this.generateRandomKeyPair(); + } + const stakerInfo = { + address: this.getAddressAndScriptPubKey(stakerKeyPair.publicKey).nativeSegwit.address, + publicKeyNoCoordHex: stakerKeyPair.publicKeyNoCoord, + publicKeyWithCoord: stakerKeyPair.publicKey, + } + params = params ? params : this.generateStakingParams(); + const timelock = this.generateRandomTimelock(params); + const finalityProviderPksNoCoordHex = this.generateRandomFidelityProviderPksNoCoordHex(); + + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + + const stakingAmountSat = stakingAmount ? + stakingAmount : this.getRandomIntegerBetween( + params.minStakingAmountSat, params.maxStakingAmountSat, + ); + + const { publicKey } = stakerKeyPair; + const { taproot, nativeSegwit } = this.getAddressAndScriptPubKey(publicKey); + const scriptPubKey = + addressType === "taproot" + ? taproot.scriptPubKey + : nativeSegwit.scriptPubKey; + + const utxos = this.generateRandomUTXOs( + this.getRandomIntegerBetween(stakingAmountSat, stakingAmountSat + 100000000), + this.getRandomIntegerBetween(1, 10), + scriptPubKey, + ); + + const { transaction: stakingTx, fee: stakingTxFee } = staking.createStakingTransaction( + stakingAmountSat, + utxos, + feeRate, + ); + + return { + stakingTx, + timelock, + stakingInstance: staking, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + stakingAmountSat, + keyPair: stakerKeyPair, + stakingTxFee, + utxos, + scriptPubKey, + } + }; + + /** + * Generates a random slashing transaction based on the staking transaction + * and staking scripts + * @param network - The network to use + * @param stakingScripts - The staking scripts to use + * @param stakingTx - The staking transaction to use + * @param params - The params used in the staking transaction + * @param keyPair - The key pair to use. This is used to sign the slashing + * psbt to derive the transaction. + * @param type - The type of slashing to use. + * @returns {Object} - A random slashing transaction + */ + generateSlashingTransaction = ( + network: bitcoin.networks.Network, + stakingScripts: StakingScripts, + stakingTx: Transaction, + params: { + minSlashingTxFeeSat: number, + slashingPkScriptHex: string, + slashingRate: number, + }, + keyPair: KeyPair, + type: SlashingType = "timelockExpire", + ) => { + let slashingPsbt: Psbt; + let outputValue: number; + + if (type === "earlyUnbonded") { + const { transaction: unbondingTx } = unbondingTransaction( + stakingScripts, + stakingTx, + 1, + network, + ); + const { psbt } = slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + params.slashingPkScriptHex, + params.slashingRate, + params.minSlashingTxFeeSat, + network, + ); + slashingPsbt = psbt; + outputValue = unbondingTx.outs[0].value; + } else { + const { psbt } = slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + params.slashingPkScriptHex, + params.slashingRate, + params.minSlashingTxFeeSat, + network, + ); + slashingPsbt = psbt; + outputValue = stakingTx.outs[0].value; + } + + expect(slashingPsbt).toBeDefined(); + expect(slashingPsbt.txOutputs.length).toBe(2); + // first output shall send slashed amount to the slashing pk script (i.e burn output) + expect(Buffer.from(slashingPsbt.txOutputs[0].script).toString("hex")).toBe( + params.slashingPkScriptHex, + ); + expect(slashingPsbt.txOutputs[0].value).toBe( + Math.round(outputValue * params.slashingRate), + ); + + // second output is the change output which send to unbonding timelock script address + const changeOutput = payments.p2tr({ + internalPubkey, + scriptTree: { output: stakingScripts.unbondingTimelockScript }, + network, + }); + expect(slashingPsbt.txOutputs[1].address).toBe(changeOutput.address); + const expectedChangeOutputValue = + outputValue - + Math.round(outputValue * params.slashingRate) - + params.minSlashingTxFeeSat; + expect(slashingPsbt.txOutputs[1].value).toBe(expectedChangeOutputValue); + + expect(slashingPsbt.version).toBe(TRANSACTION_VERSION); + expect(slashingPsbt.locktime).toBe(0); + slashingPsbt.txInputs.forEach((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + + const tx = slashingPsbt.signAllInputs( + keyPair.keyPair, + ).finalizeAllInputs().extractTransaction(); + + return { + psbt: slashingPsbt, + tx, + }; + } + + randomBoolean(): boolean { + return Math.random() >= 0.5; + }; + + generateRandomScriptPubKey = ({isTaproot}: {isTaproot?: boolean} = {}): string => { + const pk = this.generateRandomKeyPair().publicKey; + const { taproot, nativeSegwit } = this.getAddressAndScriptPubKey(pk); + if (isTaproot) { + return taproot.scriptPubKey; + } + return nativeSegwit.scriptPubKey; + }; + + private getTaprootAddress = (publicKeyWithCoord: string) => { + // Remove the prefix if it exists + let publicKeyNoCoord = ""; + if (publicKeyWithCoord.length == 66) { + publicKeyNoCoord = publicKeyWithCoord.slice(2); + } + const internalPubkey = Buffer.from(publicKeyNoCoord, "hex"); + const { address, output: scriptPubKey } = bitcoin.payments.p2tr({ + internalPubkey, + network: this.network, + }); + if (!address || !scriptPubKey) { + throw new Error( + "Failed to generate taproot address or script from public key", + ); + } + return { + address, + scriptPubKey: scriptPubKey.toString("hex"), + }; + }; + + private getNativeSegwitAddress = (publicKey: string) => { + // check the public key length is 66, otherwise throw + if (publicKey.length !== 66) { + throw new Error( + "Invalid public key length for generating native segwit address", + ); + } + const internalPubkey = Buffer.from(publicKey, "hex"); + const { address, output: scriptPubKey } = bitcoin.payments.p2wpkh({ + pubkey: internalPubkey, + network: this.network, + }); + if (!address || !scriptPubKey) { + throw new Error( + "Failed to generate native segwit address or script from public key", + ); + } + return { + address, + scriptPubKey: scriptPubKey.toString("hex"), + }; + }; + + generateRandomFidelityProviderPksNoCoordHex = ( + numberOfFidelityProviders: number = 10, + ) => { + const finalityProviderPksNoCoordHex: string[] = []; + for (let i = 0; i < numberOfFidelityProviders; i++) { + finalityProviderPksNoCoordHex.push(this.generateRandomKeyPair().publicKeyNoCoord); + } + return finalityProviderPksNoCoordHex; + } +} + +export const 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"); +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts new file mode 100644 index 0000000000..5ec738c6bc --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts @@ -0,0 +1,43 @@ +import { ObservableStakingScriptData, ObservableStakingScripts } from "../../../src/staking/observable"; +import { ObservableVersionedStakingParams } from "../../../src/types/params"; +import { StakingDataGenerator } from "./base"; + +export class ObservableStakingDatagen extends StakingDataGenerator { + generateRandomTag = () => { + const buffer = Buffer.alloc(4); + for (let i = 0; i < 4; i++) { + buffer[i] = Math.floor(Math.random() * 256); + } + return buffer; + }; + + generateStakingParams = ( + fixedTerm = false, + committeeSize?: number, + minStakingAmount?: number, + ): ObservableVersionedStakingParams => { + return { + ...super.generateStakingParams(fixedTerm, committeeSize, minStakingAmount), + btcActivationHeight: this.getRandomIntegerBetween(1000, 100000), + tag: this.generateRandomTag().toString("hex"), + version: this.getRandomIntegerBetween(1, 10), + }; + }; + + generateStakingScriptData = ( + stakerPkNoCoord: string, + params: ObservableVersionedStakingParams, + timelock: number, + ): ObservableStakingScripts => { + const fpPkHex = this.generateRandomKeyPair().publicKeyNoCoord; + return new ObservableStakingScriptData( + Buffer.from(stakerPkNoCoord, "hex"), + [Buffer.from(fpPkHex, "hex")], + params.covenantNoCoordPks.map((pk: string) => Buffer.from(pk, "hex")), + params.covenantQuorum, + timelock, + params.unbondingTime, + Buffer.from(params.tag, "hex"), + ).buildScripts(); + } +} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts new file mode 100644 index 0000000000..d974fddbd9 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts @@ -0,0 +1,2 @@ +export { default as testingNetworks } from "./testingNetworks"; +export const DEFAULT_TEST_FEE_RATE = 15; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts new file mode 100644 index 0000000000..57f605542c --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts @@ -0,0 +1,30 @@ +/** + * Generates an array of random integers for each slice that sum up to the total amount. + * + * @param totalAmount - The total amount to be distributed across the slices (must be an integer). + * @param numOfSlices - The number of slices (must be an integer). + * @returns An array of integers representing the amount for each slice. + */ +export const generateRandomAmountSlices = ( + totalAmount: number, + numOfSlices: number, +): number[] => { + if (numOfSlices <= 0) { + throw new Error("Number of slices must be greater than zero."); + } + + const amounts: number[] = []; + let remainingAmount = totalAmount; + + for (let i = 0; i < numOfSlices - 1; i++) { + const max = Math.floor(remainingAmount / (numOfSlices - i)); + const amount = Math.floor(Math.random() * max); + amounts.push(amount); + remainingAmount -= amount; + } + + // Push the remaining amount as the last slice amount + amounts.push(remainingAmount); + + return amounts; +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts new file mode 100644 index 0000000000..5a6d9789ff --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts @@ -0,0 +1,30 @@ +import * as bitcoin from "bitcoinjs-lib"; +import { StakingDataGenerator } from "./datagen/base"; + +export interface NetworkConfig { + networkName: string; + network: bitcoin.Network; + datagen: { + stakingDatagen: StakingDataGenerator; + } +} + +const createNetworkConfig = ( + networkName: string, + network: bitcoin.Network, +): NetworkConfig => ({ + networkName, + // A deep copy of the network object to avoid referring to the same object + // in memory + network: {...network}, + datagen: { + stakingDatagen: new StakingDataGenerator(network), + }, +}); + +const testingNetworks: NetworkConfig[] = [ + createNetworkConfig("mainnet", bitcoin.networks.bitcoin), + createNetworkConfig("testnet", bitcoin.networks.testnet), +]; + +export default testingNetworks; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts new file mode 100644 index 0000000000..6c90453c81 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts @@ -0,0 +1,169 @@ +import { Buffer } from "buffer"; +import { createCovenantWitness } from "../../src"; + +describe("createCovenantWitness", () => { + it("should return the correct witness when multiple covenants are matched", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + Buffer.from("covenant2", "utf-8"), + ]; + const covenantSigs = [ + // 'covenant1' and 'signature1' in hex + { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, + // 'covenant2' and 'signature2' in hex + { btcPkHex: "636f76656e616e7432", sigHex: "7369676e617475726532" }, + ]; + const covenantQuorum = 2; + + const result = createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + ); + + expect(result).toEqual([ + Buffer.from("7369676e617475726532", "hex"), // 'signature2' in hex + Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex + ...originalWitness, + ]); + }); + + it("should throw error if not enough covenant signatures", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + Buffer.from("covenant2", "utf-8"), + ]; + const covenantSigs = [ + // 'covenant1' and 'signature1' in hex + { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, + ]; + const covenantQuorum = 2; + + expect(() => createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + )).toThrow("Not enough covenant signatures. Required: 2, got: 1"); + }); + + it("should throw error if not enough valid covenant signatures after filtering", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + Buffer.from("covenant2", "utf-8"), + ]; + const covenantSigs = [ + // Valid signature for covenant1 + { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, + // Invalid signature - doesn't match any params covenant + { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, + ]; + const covenantQuorum = 2; + + expect(() => createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + )).toThrow("Not enough valid covenant signatures. Required: 2, got: 1"); + }); + + it("should throw error if all covenant signatures are invalid", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + Buffer.from("covenant2", "utf-8"), + ]; + const covenantSigs = [ + // Invalid signature - doesn't match any params covenant + { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, + // Another invalid signature + { btcPkHex: "616e6f74686572696e76", sigHex: "616e6f74686572736967" }, + ]; + const covenantQuorum = 2; + + expect(() => createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + )).toThrow("Not enough valid covenant signatures. Required: 2, got: 0"); + }); + + it("should work with mixed valid and invalid signatures when enough valid ones exist", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + Buffer.from("covenant2", "utf-8"), + ]; + const covenantSigs = [ + // Valid signature for covenant1 + { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, + // Valid signature for covenant2 + { btcPkHex: "636f76656e616e7432", sigHex: "7369676e617475726532" }, + // Invalid signature - should be ignored + { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, + ]; + const covenantQuorum = 2; + + const result = createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + ); + + expect(result).toEqual([ + Buffer.from("7369676e617475726532", "hex"), // 'signature2' in hex + Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex + ...originalWitness, + ]); + }); + + it("should work with quorum of 1 when exactly one valid signature is provided", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + ]; + const covenantSigs = [ + // Valid signature for covenant1 + { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, + ]; + const covenantQuorum = 1; + + const result = createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + ); + + expect(result).toEqual([ + Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex + ...originalWitness, + ]); + }); + + it("should throw error when quorum is 1 but no valid signatures are provided", () => { + const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; + const paramsCovenants = [ + Buffer.from("covenant1", "utf-8"), + ]; + const covenantSigs = [ + // Invalid signature - doesn't match params covenant + { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, + ]; + const covenantQuorum = 1; + + expect(() => createCovenantWitness( + originalWitness, + paramsCovenants, + covenantSigs, + covenantQuorum + )).toThrow("Not enough valid covenant signatures. Required: 1, got: 0"); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts new file mode 100644 index 0000000000..dae371b62a --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts @@ -0,0 +1,145 @@ +import * as stakingScript from "../../src/staking/stakingScript"; +import { testingNetworks } from "../helper"; +import * as transaction from "../../src/staking/transactions"; +import { opcodes, payments, script } from "bitcoinjs-lib"; +import { internalPubkey } from "../../src/constants/internalPubkey"; + +describe.each(testingNetworks)("Create slashing transactions", ({ + network, networkName, datagen: { stakingDatagen: dataGenerator } +}) => { + const { + stakingTx, stakingInstance, + stakerInfo, params, stakingAmountSat, + } = dataGenerator.generateRandomStakingTransaction( + network, 1 + ); + + const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction( + stakingTx, + ); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + describe("Create slash early unbonded transaction", () => { + it(`${networkName} should throw an error if fail to build scripts`, () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new Error("slash early unbonded delegation build script error"); + }); + + expect(() => stakingInstance.createUnbondingOutputSlashingPsbt( + unbondingTx, + )).toThrow("slash early unbonded delegation build script error"); + }); + + it(`${networkName} should throw an error if fail to build early unbonded slash tx`, () => { + jest.spyOn(transaction, "slashEarlyUnbondedTransaction").mockImplementation(() => { + throw new Error("fail to build slash tx"); + }); + expect(() => stakingInstance.createUnbondingOutputSlashingPsbt( + unbondingTx, + )).toThrow("fail to build slash tx"); + }); + + it(`${networkName} should create slash early unbonded transaction`, () => { + const slashTx = stakingInstance.createUnbondingOutputSlashingPsbt( + unbondingTx, + ); + expect(slashTx.psbt.txInputs.length).toBe(1) + expect(slashTx.psbt.txInputs[0].hash.toString("hex")). + toBe(unbondingTx.getHash().toString("hex")); + expect(slashTx.psbt.txInputs[0].index).toBe(0); + // verify outputs + expect(slashTx.psbt.txOutputs.length).toBe(2); + // slash amount + const stakingAmountLeftInUnbondingTx = unbondingTx.outs[0].value; + const slashAmount = Math.round(stakingAmountLeftInUnbondingTx * params.slashing!.slashingRate); + expect(slashTx.psbt.txOutputs[0].value).toBe( + slashAmount, + ); + expect(Buffer.from(slashTx.psbt.txOutputs[0].script).toString("hex")).toBe( + params.slashing!.slashingPkScriptHex + ); + // change output + const unbondingTimelockScript = script.compile([ + Buffer.from(stakerInfo.publicKeyNoCoordHex, "hex"), + opcodes.OP_CHECKSIGVERIFY, + script.number.encode(params.unbondingTime), + opcodes.OP_CHECKSEQUENCEVERIFY, + ]); + const { address } = payments.p2tr({ + internalPubkey, + scriptTree: { output: unbondingTimelockScript }, + network, + }); + expect(slashTx.psbt.txOutputs[1].address).toBe(address); + const userFunds = stakingAmountLeftInUnbondingTx - slashAmount - params.slashing!.minSlashingTxFeeSat; + expect(slashTx.psbt.txOutputs[1].value).toBe(userFunds); + expect(slashTx.psbt.locktime).toBe(0); + expect(slashTx.psbt.version).toBe(2); + }); + }); + + describe("Create slash timelock unbonded transaction", () => { + it(`${networkName} should throw an error if fail to build scripts`, async () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new Error("slash timelock unbonded delegation build script error"); + }); + + expect(() => stakingInstance.createStakingOutputSlashingPsbt( + stakingTx, + )).toThrow("slash timelock unbonded delegation build script error"); + }); + + it(`${networkName} should throw an error if fail to build timelock unbonded slash tx`, async () => { + jest.spyOn(transaction, "slashTimelockUnbondedTransaction").mockImplementation(() => { + throw new Error("fail to build slash tx"); + }); + + expect(() => stakingInstance.createStakingOutputSlashingPsbt( + stakingTx, + )).toThrow("fail to build slash tx"); + }); + + it(`${networkName} should create slash timelock unbonded transaction`, async () => { + const slashTx = stakingInstance.createStakingOutputSlashingPsbt( + stakingTx, + ); + expect(slashTx.psbt.txInputs.length).toBe(1) + expect(slashTx.psbt.txInputs[0].hash.toString("hex")). + toBe(stakingTx.getHash().toString("hex")); + expect(slashTx.psbt.txInputs[0].index).toBe(0); + // verify outputs + expect(slashTx.psbt.txOutputs.length).toBe(2); + // slash amount + const slashAmount = Math.round(stakingAmountSat * params.slashing!.slashingRate); + expect(slashTx.psbt.txOutputs[0].value).toBe( + slashAmount, + ); + expect(Buffer.from(slashTx.psbt.txOutputs[0].script).toString("hex")).toBe( + params.slashing!.slashingPkScriptHex + ); + // change output + const unbondingTimelockScript = script.compile([ + Buffer.from(stakerInfo.publicKeyNoCoordHex, "hex"), + opcodes.OP_CHECKSIGVERIFY, + script.number.encode(params.unbondingTime), + opcodes.OP_CHECKSEQUENCEVERIFY, + ]); + const { address } = payments.p2tr({ + internalPubkey, + scriptTree: { output: unbondingTimelockScript }, + network, + }); + expect(slashTx.psbt.txOutputs[1].address).toBe(address); + const userFunds = stakingAmountSat - slashAmount - params.slashing!.minSlashingTxFeeSat; + expect(slashTx.psbt.txOutputs[1].value).toBe(userFunds); + expect(slashTx.psbt.locktime).toBe(0); + expect(slashTx.psbt.version).toBe(2); + }); + }); +}); + diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts new file mode 100644 index 0000000000..118b5b2eab --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts @@ -0,0 +1,248 @@ +import { address } from "bitcoinjs-lib"; +import * as stakingScript from "../../src/staking/stakingScript"; +import { testingNetworks } from "../helper"; +import { StakingParams } from "../../src/types/params"; +import { UTXO } from "../../src/types/UTXO"; +import { StakingError, StakingErrorCode } from "../../src/error"; +import { BTC_DUST_SAT } from "../../src/constants/dustSat"; +import { NON_RBF_SEQUENCE } from "../../src/constants/psbt"; +import * as stakingUtils from "../../src/utils/staking/validation"; +import * as stakingTx from "../../src/staking/transactions"; +import { transactionIdToHash } from "../../src"; +import { Staking } from "../../src/staking"; + +describe.each(testingNetworks)("Create staking transaction", ({ + network, networkName, datagen: { stakingDatagen: dataGenerator } +}) => { + let stakerInfo: { address: string, publicKeyNoCoordHex: string, publicKeyWithCoord: string }; + let params: StakingParams; + let timelock: number; + let utxos: UTXO[]; + let finalityProviderPksNoCoordHex: string[] = []; + const feeRate = 1; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + + const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); + const { address, scriptPubKey } = dataGenerator.getAddressAndScriptPubKey( + publicKey, + ).taproot; + + stakerInfo = { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + publicKeyWithCoord: publicKey, + }; + // random number of FPs + for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { + finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); + } + params = dataGenerator.generateStakingParams(true); + timelock = dataGenerator.generateRandomTimelock(params); + utxos = dataGenerator.generateRandomUTXOs( + params.maxStakingAmountSat * dataGenerator.getRandomIntegerBetween(1, 100), + dataGenerator.getRandomIntegerBetween(1, 10), + scriptPubKey, + ); + }); + + it(`${networkName} throw StakingError if stakerInfo is incorrect`, async () => { + const stakerInfoWithCoordPk = { + address: stakerInfo.address, + publicKeyNoCoordHex: stakerInfo.publicKeyWithCoord, + }; + expect(() => new Staking( + network, stakerInfoWithCoordPk, + params, finalityProviderPksNoCoordHex, timelock, + )).toThrow( + "Invalid staker public key" + ); + + const stakerInfoWithInvalidAddress = { + address: "abc", + publicKeyNoCoordHex: stakerInfo.publicKeyNoCoordHex, + }; + expect(() => new Staking( + network, stakerInfoWithInvalidAddress, + params, finalityProviderPksNoCoordHex, timelock, + )).toThrow( + "Invalid staker bitcoin address" + ); + }); + + it(`${networkName} should throw an error if input data validation failed`, async () => { + jest.spyOn(stakingUtils, "validateStakingTxInputData").mockImplementation(() => { + throw new StakingError(StakingErrorCode.INVALID_INPUT, "some error"); + }); + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + + expect(() => staking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.INVALID_INPUT, "some error") + ); + }); + + it(`${networkName} should throw an error if fail to build scripts`, async () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error"); + }); + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + + expect(() => staking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error") + ); + }); + + it(`${networkName} should throw an error if fail to build staking tx`, async () => { + jest.spyOn(stakingTx, "stakingTransaction").mockImplementation(() => { + throw new Error("fail to build staking tx"); + }); + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + + expect(() => staking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.BUILD_TRANSACTION_FAILURE, "fail to build staking tx") + ); + }); + + it(`${networkName} should throw an error if fail to validate staking output`, async () => { + // Setup + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + const amount = dataGenerator.getRandomIntegerBetween( + params.minStakingAmountSat, params.maxStakingAmountSat, + ); + + // Create transaction and psbt + const { transaction } = staking.createStakingTransaction( + amount, + utxos, + feeRate, + ); + + // Setup a different param + const wrongParams = dataGenerator.generateStakingParams(); + const wrongTimelock = dataGenerator.generateRandomTimelock(wrongParams); + const wrongStaking = new Staking( + network, stakerInfo, + wrongParams, finalityProviderPksNoCoordHex, wrongTimelock, + ); + + expect(() => wrongStaking.toStakingPsbt(transaction, utxos)).toThrow( + expect.objectContaining({ + code: StakingErrorCode.INVALID_OUTPUT, + message: expect.stringContaining("Matching output not found") + }) + ); + }); + + it(`${networkName} should successfully create a staking transaction & psbt`, async () => { + // Setup + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + const amount = dataGenerator.getRandomIntegerBetween( + params.minStakingAmountSat, params.maxStakingAmountSat, + ); + + // Create transaction and psbt + const { transaction, fee } = staking.createStakingTransaction( + amount, + utxos, + feeRate, + ); + const psbt = staking.toStakingPsbt(transaction, utxos); + + // Basic validation + expect(transaction).toBeDefined(); + expect(fee).toBeGreaterThan(0); + expect(transaction.version).toBe(2); + expect(psbt.version).toBe(2); + + // Validate inputs + expect(transaction.ins.length).toBeGreaterThan(0); + expect(psbt.data.inputs.length).toBe(transaction.ins.length); + expect(psbt.data.inputs[0].tapInternalKey?.toString("hex")).toEqual(stakerInfo.publicKeyNoCoordHex); + expect(psbt.data.inputs[0].witnessUtxo?.script.toString("hex")).toEqual(utxos[0].scriptPubKey); + + // Validate sequences + transaction.ins.forEach(input => expect(input.sequence).toBe(NON_RBF_SEQUENCE)); + psbt.txInputs.forEach(input => expect(input.sequence).toBe(NON_RBF_SEQUENCE)); + + // Calculate and validate amounts + const psbtInputAmount = psbt.data.inputs.reduce((sum, input) => + sum + (input.witnessUtxo?.value || 0), 0); + const txInputAmount = transaction.ins.reduce((sum, input) => { + const matchingUtxo = utxos.find(utxo => + transactionIdToHash(utxo.txid).toString("hex") === input.hash.toString("hex") + && utxo.vout === input.index); + return sum + (matchingUtxo?.value || 0); + }, 0); + + expect(psbtInputAmount).toBeGreaterThanOrEqual(amount + fee); + expect(txInputAmount).toBeGreaterThanOrEqual(amount + fee); + + // Validate change outputs if present + const psbtChangeAmount = psbtInputAmount - amount - fee; + const txChangeAmount = txInputAmount - amount - fee; + expect(psbtChangeAmount).toEqual(txChangeAmount); + + if (psbtChangeAmount > BTC_DUST_SAT) { + const lastPsbtOutput = psbt.txOutputs[psbt.txOutputs.length - 1]; + const lastTxOutput = transaction.outs[transaction.outs.length - 1]; + + expect(lastPsbtOutput.value).toEqual(psbtChangeAmount); + expect(lastPsbtOutput.address).toEqual(stakerInfo.address); + expect(lastTxOutput.value).toEqual(txChangeAmount); + expect(lastTxOutput.script).toEqual(address.toOutputScript(stakerInfo.address, network)); + } + + // Validate staking amount output + expect(psbt.txOutputs[0].value).toEqual(amount); + expect(transaction.outs[0].value).toEqual(amount); + + // Validate transaction and psbt match + expect(psbt.locktime).toEqual(transaction.locktime); + expect(psbt.txOutputs.length).toEqual(transaction.outs.length); + + // Validate all inputs match between psbt and transaction + psbt.txInputs.forEach((input, i) => { + const txInput = transaction.ins[i]; + expect(input.hash).toEqual(txInput.hash); + expect(input.index).toEqual(txInput.index); + expect(input.sequence).toEqual(txInput.sequence); + }); + + // Validate all outputs match between psbt and transaction + psbt.txOutputs.forEach((output, i) => { + const txOutput = transaction.outs[i]; + expect(output.value).toEqual(txOutput.value); + expect(output.script).toEqual(txOutput.script); + }); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts new file mode 100644 index 0000000000..6056758d8f --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts @@ -0,0 +1,102 @@ +import { Staking } from "../../src/staking"; +import * as transaction from "../../src/staking/transactions"; +import { internalPubkey } from "../../src/constants/internalPubkey"; +import { StakingError, StakingErrorCode } from "../../src/error"; +import { testingNetworks } from "../helper"; +import { NON_RBF_SEQUENCE } from "../../src/constants/psbt"; +import * as stakingScript from "../../src/staking/stakingScript"; +import { deriveStakingOutputInfo, findMatchingTxOutputIndex } from "../../src/utils/staking"; + +describe.each(testingNetworks)("Create unbonding transaction", ({ + network, networkName, datagen: { stakingDatagen : dataGenerator } +}) => { + const feeRate = 1; + const { + stakingTx, + timelock, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + stakingAmountSat, + } = dataGenerator.generateRandomStakingTransaction( + network, + feeRate, + ); + let staking: Staking; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + }); + + it(`${networkName} should throw an error if fail to build scripts`, async () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "build script error"); + }); + + expect(() => staking.createUnbondingTransaction( + stakingTx, + )).toThrow("build script error"); + }); + + it(`${networkName} should throw an error if fail to build unbonding tx`, async () => { + jest.spyOn(transaction, "unbondingTransaction").mockImplementation(() => { + throw new Error("fail to build unbonding tx"); + }); + expect(() => staking.createUnbondingTransaction( + stakingTx, + )).toThrow("fail to build unbonding tx"); + }); + + it(`${networkName} should successfully create an unbonding transaction & psbt`, async () => { + // Create transaction and psbt + const { transaction } = staking.createUnbondingTransaction(stakingTx); + const scripts = staking.buildScripts(); + const psbt = staking.toUnbondingPsbt(transaction, stakingTx); + + // Basic validation + expect(transaction.version).toBe(2); + expect(psbt.version).toBe(2); + expect(transaction.locktime).toBe(0); + expect(psbt.locktime).toBe(0); + + // Get staking output index + const stakingOutputIndex = findMatchingTxOutputIndex( + stakingTx, + deriveStakingOutputInfo(scripts, network).outputAddress, + network, + ); + + // Validate inputs + expect(transaction.ins.length).toBe(1); + expect(psbt.data.inputs.length).toBe(1); + expect(transaction.ins[0].hash).toEqual(stakingTx.getHash()); + expect(psbt.txInputs[0].hash).toEqual(stakingTx.getHash()); + expect(transaction.ins[0].index).toEqual(stakingOutputIndex); + expect(psbt.txInputs[0].index).toEqual(stakingOutputIndex); + expect(transaction.ins[0].sequence).toEqual(NON_RBF_SEQUENCE); + expect(psbt.txInputs[0].sequence).toEqual(NON_RBF_SEQUENCE); + + // Validate PSBT input details + expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); + expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); + expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(stakingAmountSat); + expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( + stakingTx.outs[stakingOutputIndex].script + ); + + // Validate outputs + expect(transaction.outs.length).toBe(1); + expect(psbt.txOutputs.length).toBe(1); + expect(transaction.outs[0].value).toEqual(stakingAmountSat - params.unbondingFeeSat); + expect(psbt.txOutputs[0].value).toEqual(stakingAmountSat - params.unbondingFeeSat); + + // Validate transaction and psbt match + expect(psbt.txOutputs[0].script).toEqual(transaction.outs[0].script); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts new file mode 100644 index 0000000000..6c962b2414 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts @@ -0,0 +1,112 @@ +import * as stakingScript from "../../src/staking/stakingScript"; +import { testingNetworks } from "../helper"; +import * as transaction from "../../src/staking/transactions"; +import { getWithdrawTxFee } from "../../src/utils/fee"; + +describe.each(testingNetworks)("Create withdrawal transactions", ({ + network, networkName, datagen: { stakingDatagen: dataGenerator } +}) => { + const feeRate = 1; + const { + stakingTx, + stakerInfo, + stakingInstance, + } = dataGenerator.generateRandomStakingTransaction( + network, feeRate, + ); + + const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction( + stakingTx, + ); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + describe("Create withdraw early unbonded transaction", () => { + it(`${networkName} should throw an error if fail to build scripts`, () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new Error("withdraw early unbonded delegation build script error"); + }); + + expect(() => stakingInstance.createWithdrawEarlyUnbondedTransaction( + unbondingTx, + feeRate, + )).toThrow("withdraw early unbonded delegation build script error"); + }); + + it(`${networkName} should throw an error if fail to build early unbonded withdraw tx`, () => { + jest.spyOn(transaction, "withdrawEarlyUnbondedTransaction").mockImplementation(() => { + throw new Error("fail to build withdraw tx"); + }); + expect(() => stakingInstance.createWithdrawEarlyUnbondedTransaction( + unbondingTx, + feeRate, + )).toThrow("fail to build withdraw tx"); + }); + + it(`${networkName} should create withdraw early unbonded transaction`, () => { + const withdrawTx = stakingInstance.createWithdrawEarlyUnbondedTransaction( + unbondingTx, + feeRate, + ); + expect(withdrawTx.psbt.txInputs.length).toBe(1) + expect(withdrawTx.psbt.txInputs[0].hash.toString("hex")). + toBe(unbondingTx.getHash().toString("hex")); + expect(withdrawTx.psbt.txInputs[0].index).toBe(0); + expect(withdrawTx.psbt.txOutputs.length).toBe(1); + const fee = getWithdrawTxFee(feeRate); + expect(withdrawTx.psbt.txOutputs[0].value).toBe( + unbondingTx.outs[0].value - fee, + ); + expect(withdrawTx.psbt.txOutputs[0].address).toBe(stakerInfo.address); + expect(withdrawTx.psbt.locktime).toBe(0); + expect(withdrawTx.psbt.version).toBe(2); + }); + }); + + describe("Create timelock unbonded transaction", () => { + it(`${networkName} should throw an error if fail to build scripts`, async () => { + jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { + throw new Error("withdraw timelock unbonded delegation build script error"); + }); + expect(() => stakingInstance.createWithdrawStakingExpiredPsbt( + stakingTx, + feeRate, + )).toThrow("withdraw timelock unbonded delegation build script error"); + }); + + it(`${networkName} should throw an error if fail to build timelock unbonded withdraw tx`, async () => { + jest.spyOn(transaction, "withdrawTimelockUnbondedTransaction").mockImplementation(() => { + throw new Error("fail to build withdraw tx"); + }); + + expect(() => stakingInstance.createWithdrawStakingExpiredPsbt( + stakingTx, + feeRate, + )).toThrow("fail to build withdraw tx"); + }); + + it(`${networkName} should create withdraw timelock unbonded transaction`, async () => { + const withdrawTx = stakingInstance.createWithdrawStakingExpiredPsbt( + stakingTx, + feeRate, + ); + expect(withdrawTx.psbt.txInputs.length).toBe(1) + expect(withdrawTx.psbt.txInputs[0].hash.toString("hex")). + toBe(stakingTx.getHash().toString("hex")); + expect(withdrawTx.psbt.txInputs[0].index).toBe(0); + expect(withdrawTx.psbt.txOutputs.length).toBe(1); + const fee = getWithdrawTxFee(feeRate); + expect(withdrawTx.psbt.txOutputs[0].value).toBe( + stakingTx.outs[0].value - fee, + ); + expect(withdrawTx.psbt.txOutputs[0].address).toBe(stakerInfo.address); + expect(withdrawTx.psbt.locktime).toBe(0); + expect(withdrawTx.psbt.version).toBe(2); + }); + }); +}); + diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts new file mode 100644 index 0000000000..7705d91275 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts @@ -0,0 +1,233 @@ +import { + getPublicKeyNoCoord, + VersionedStakingParams, + type UTXO, +} from "../../../../src"; + +export const stakerInfo = { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", + ), + address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", +}; + +export const stakerInfoArr = [ + // Taproot + stakerInfo, + // Native SegWit + { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "03d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87", + ), + address: "tb1qlphktyz6sse3meq36pjwjrsqktny4553paydg2", + }, + // Legacy + { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "028333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37", + ), + address: "msSV7NptGswtM4k7Qom6f9efJ2rcZQQ8Ho", + }, +]; + +export const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; + +export const stakingInput = { + stakingAmountSat: 500_000, + finalityProviderPksNoCoordHex: [ + getPublicKeyNoCoord( + "d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76", + ), + ], + stakingTimelock: 64000, +}; + +export const utxos: UTXO[] = [ + { + scriptPubKey: + "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", + txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", + value: 8586757, + vout: 1, + }, +]; + +export const btcTipHeight = 900_000; +export const invalidStartHeightArr = [ + [0, "Babylon BTC tip height cannot be 0"], + [200_000, "Babylon params not found for height 200000"], +] as [number, string][]; + +export const feeRate = 4; + +export const stakingParams: VersionedStakingParams[] = [ + { + version: 0, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 5000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 64000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 857910, + }, + { + version: 1, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 50000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 864790, + }, + { + version: 2, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 874088, + }, + { + version: 3, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 891425, + }, + { + version: 4, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 9600, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 893362, + }, +].map((v) => ({ + version: v.version, + covenantNoCoordPks: v.covenant_pks.map((pk) => + String(getPublicKeyNoCoord(pk)), + ), + covenantQuorum: v.covenant_quorum, + minStakingValueSat: v.min_staking_value_sat, + maxStakingValueSat: v.max_staking_value_sat, + minStakingTimeBlocks: v.min_staking_time_blocks, + maxStakingTimeBlocks: v.max_staking_time_blocks, + unbondingTime: v.unbonding_time_blocks, + unbondingFeeSat: v.unbonding_fee_sat, + minCommissionRate: v.min_commission_rate, + maxActiveFinalityProviders: v.max_active_finality_providers, + delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, + slashing: { + slashingPkScriptHex: v.slashing_pk_script, + slashingRate: parseFloat(v.slashing_rate), + minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, + }, + maxStakingAmountSat: v.max_staking_value_sat, + minStakingAmountSat: v.min_staking_value_sat, + btcActivationHeight: v.btc_activation_height, + allowListExpirationHeight: v.allow_list_expiration_height, +})); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts new file mode 100644 index 0000000000..e51d97c90c --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts @@ -0,0 +1,13 @@ +export const btcProvider = { + signPsbt: jest.fn(), + signMessage: jest.fn(), + getTransactionHex: jest.fn(), +}; + +export const babylonProvider = { + signTransaction: jest.fn(), + getCurrentHeight: jest.fn(), + getChainId: jest.fn(), +}; + +export const mockChainId = "bbn-1"; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts new file mode 100644 index 0000000000..999e85853d --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts @@ -0,0 +1,494 @@ +import { Transaction } from "bitcoinjs-lib"; +import { + getPublicKeyNoCoord, + VersionedStakingParams, + type UTXO, +} from "../../../../src"; + +export const params: VersionedStakingParams[] = [ + { + version: 0, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 5000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 64000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 857910, + }, + { + version: 1, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 50000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 864790, + }, + { + version: 2, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 874088, + }, + { + version: 3, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 32000, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 891425, + }, + { + version: 4, + covenant_pks: [ + "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", + "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", + "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", + "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", + "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", + "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", + "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", + "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", + "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", + ], + covenant_quorum: 6, + min_staking_value_sat: 500000, + max_staking_value_sat: 500000000000, + min_staking_time_blocks: 64000, + max_staking_time_blocks: 64000, + slashing_pk_script: "6a07626162796c6f6e", + min_slashing_tx_fee_sat: 100000, + slashing_rate: "0.001000000000000000", + unbonding_time_blocks: 1008, + unbonding_fee_sat: 9600, + min_commission_rate: "0.030000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1095000, + allow_list_expiration_height: 139920, + btc_activation_height: 893362, + }, +].map((v) => ({ + version: v.version, + covenantNoCoordPks: v.covenant_pks.map((pk) => + String(getPublicKeyNoCoord(pk)), + ), + covenantQuorum: v.covenant_quorum, + minStakingValueSat: v.min_staking_value_sat, + maxStakingValueSat: v.max_staking_value_sat, + minStakingTimeBlocks: v.min_staking_time_blocks, + maxStakingTimeBlocks: v.max_staking_time_blocks, + unbondingTime: v.unbonding_time_blocks, + unbondingFeeSat: v.unbonding_fee_sat, + minCommissionRate: v.min_commission_rate, + maxActiveFinalityProviders: v.max_active_finality_providers, + delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, + slashing: { + slashingPkScriptHex: v.slashing_pk_script, + slashingRate: parseFloat(v.slashing_rate), + minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, + }, + maxStakingAmountSat: v.max_staking_value_sat, + minStakingAmountSat: v.min_staking_value_sat, + btcActivationHeight: v.btc_activation_height, + allowListExpirationHeight: v.allow_list_expiration_height, +})); + +export const stakerInfo = { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", + ), + address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", +}; + +export const stakerInfoArr = [ + // Taproot + [ + stakerInfo, + { + slashingPsbt: + "70736274ff010070020000000197e5f77c011a657e5f3aa24d46c1b3e4949980a8e30b5d5555bfdbb929a7fae90000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012b20a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d0934215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac07ffc89a815b7b26da44c800e92dcf548694fd65486abe250fb6f7b30b73b2286fd7901200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + unbondingSlashingPsbt: + "70736274ff010070020000000151c1c42cbc4b725a5fa513ba3f10c1f6b5b6225f6446cd5ca61ea7e2e8dfdaea0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012ba07b070000000000225120655759c640a9d374e949e6e2cefdb6bee32e54b7dac9a0995fe508a04b3fd2cd4215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0010700255627c84b08e73ce57938ce8e6b01de0613e3a9dbb7216e9095d9129cfd7901200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + stakingTxHex: + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d09379627b0000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", + signType: "bip322-simple", + signedBabylonAddress: + "AUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", + signedSlashingPsbt: + "70736274ff010070020000000197e5f77c011a657e5f3aa24d46c1b3e4949980a8e30b5d5555bfdbb929a7fae90000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012b20a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d0930108fdff0103400fceade8b5e88c87305dda3a821e93916a0295c32fd9ad87e8b60fddc931a6aea4a78c690c979489a52dff8185c984d3e3a41bcd017d9633b2f744adb567d7f6fd7801200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac07ffc89a815b7b26da44c800e92dcf548694fd65486abe250fb6f7b30b73b2286000000", + signedUnbondingSlashingPsbt: + "70736274ff010070020000000151c1c42cbc4b725a5fa513ba3f10c1f6b5b6225f6446cd5ca61ea7e2e8dfdaea0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012ba07b070000000000225120655759c640a9d374e949e6e2cefdb6bee32e54b7dac9a0995fe508a04b3fd2cd0108fdff01034042d62d7dc274006463429df97d6e633dc98ea77f09ac3ce49d487fe06ac5beae2899b451891580cfa5a68f227aa63d15995dce710d44918a47fd1220525393a4fd7801200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0010700255627c84b08e73ce57938ce8e6b01de0613e3a9dbb7216e9095d9129c000000", + postStakingDelegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "BIP322", + btcSig: + "Cj50YjFwbHFnNDR3bHV3NjZ2cGtmY2N6MjNyZG10bGVwbngybTN5ZWY1N3l5ejY2ZmxneGRmNGg4cTd3dTZwZhJCAUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", + }, + btcPk: "CHSHYUf9dSLWF+g7+EX3+0mBUg48L3Sa1KLKG9Zg7ww=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIHReA5RzC9IKCnkAae6yi02pX3PqHRITdKKZ2NqcttCTeWJ7AAAAAAAiUSD4EVq7/Ha0wNk4wJURt2v+QzMrcSZTTxCC1pP0GamtzgAAAAA=", + stakingTxInclusionProof: { + key: { + index: 182, + hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", + }, + proof: + "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", + }, + slashingTx: + "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", + delegatorSlashingSig: + "D86t6LXojIcwXdo6gh6TkWoClcMv2a2H6LYP3ckxpq6kp4xpDJeUiaUt/4GFyYTT46QbzQF9ljOy90SttWfX9g==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wGgewcAAAAAACJRIGVXWcZAqdN06Unm4s79tr7jLlS32smgmV/lCKBLP9LNAAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAFRwcQsvEtyWl+lE7o/EMH2tbYiX2RGzVymHqfi6N/a6gAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", + delegatorUnbondingSlashingSig: + "QtYtfcJ0AGRjQp35fW5jPcmOp38JrDzknUh/4GrFvq4ombRRiRWAz6WmjyJ6pj0VmV3OcQ1EkYpH/RIgUlOTpA==", + }, + delegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "BIP322", + btcSig: + "Cj50YjFwbHFnNDR3bHV3NjZ2cGtmY2N6MjNyZG10bGVwbngybTN5ZWY1N3l5ejY2ZmxneGRmNGg4cTd3dTZwZhJCAUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", + }, + btcPk: "CHSHYUf9dSLWF+g7+EX3+0mBUg48L3Sa1KLKG9Zg7ww=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIHReA5RzC9IKCnkAae6yi02pX3PqHRITdKKZ2NqcttCTeWJ7AAAAAAAiUSD4EVq7/Ha0wNk4wJURt2v+QzMrcSZTTxCC1pP0GamtzgAAAAA=", + slashingTx: + "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", + delegatorSlashingSig: + "D86t6LXojIcwXdo6gh6TkWoClcMv2a2H6LYP3ckxpq6kp4xpDJeUiaUt/4GFyYTT46QbzQF9ljOy90SttWfX9g==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wGgewcAAAAAACJRIGVXWcZAqdN06Unm4s79tr7jLlS32smgmV/lCKBLP9LNAAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAFRwcQsvEtyWl+lE7o/EMH2tbYiX2RGzVymHqfi6N/a6gAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", + delegatorUnbondingSlashingSig: + "QtYtfcJ0AGRjQp35fW5jPcmOp38JrDzknUh/4GrFvq4ombRRiRWAz6WmjyJ6pj0VmV3OcQ1EkYpH/RIgUlOTpA==", + }, + }, + ], + // Native SegWit + [ + { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "03d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87", + ), + address: "tb1qlphktyz6sse3meq36pjwjrsqktny4553paydg2", + }, + { + slashingPsbt: + "70736274ff0100700200000001327d023e95b159b61998643f0c0f91ab2d4398e32b9030bcd7c6d80203f50d8f0000000000ffffffff02f401000000000000096a07626162796c6f6e8c18060000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012b20a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df334215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac043ed89d71b74d182f5dd7c29c9a85ff886dd243d000c4f02d019471fdf28be98fd79012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + unbondingSlashingPsbt: + "70736274ff01007002000000016864d6be973dd95a1efdf1832d93a1f15a481b08fa42a4efdcb8119155798fad0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f3050000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012ba07b0700000000002251209bfbdfbce192c6ad5ce1a9a5ec8f7b6d57d47a860de78cf9a5bcd0d061d8353f4215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08617477ba76d85cabf3d9e16ad2d85fe82d4ee485969a625cfe042932e91e0b6fd79012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + stakingTxHex: + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df3379627b0000000000160014f86f65905a84331de411d064e90e00b2e64ad29100000000", + signedBabylonAddress: + "AkgwRQIhAMxETaJ91QWSJOqFTECfvQMJID4gcIZ2DCUoRq0RrzeKAiAavtkp74/7B5u5N9T9g5l4/ZqRxXdsmdK4WeZga3rfhQEhAwPWeByOmsb9NT6XmX2QvvoIgsPgJ6cqsSr6ulw5HlqH", + signType: "bip322-simple", + signedSlashingPsbt: + "70736274ff0100700200000001327d023e95b159b61998643f0c0f91ab2d4398e32b9030bcd7c6d80203f50d8f0000000000ffffffff02f401000000000000096a07626162796c6f6e8c18060000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012b20a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df330108fdff010340f215a923800d21909fc14387d6f27495ae0c4beeba5804aafee5737ff5d33670777f3f93e653fd0cf11ba5987d46f704448320969df853481415af4e38c78c28fd78012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac043ed89d71b74d182f5dd7c29c9a85ff886dd243d000c4f02d019471fdf28be98000000", + signedUnbondingSlashingPsbt: + "70736274ff01007002000000016864d6be973dd95a1efdf1832d93a1f15a481b08fa42a4efdcb8119155798fad0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f3050000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012ba07b0700000000002251209bfbdfbce192c6ad5ce1a9a5ec8f7b6d57d47a860de78cf9a5bcd0d061d8353f0108fdff01034058f5855f757e8e7b6913522d1118cad511cf17f6769875ba2260b989123cd7f450a537399859e3c817ac05c3938da076d28f3c385d4fcdd3856de30f4c288ea4fd78012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08617477ba76d85cabf3d9e16ad2d85fe82d4ee485969a625cfe042932e91e0b6000000", + postStakingDelegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "BIP322", + btcSig: + "Cip0YjFxbHBoa3R5ejZzc2UzbWVxMzZwandqcnNxa3RueTQ1NTNwYXlkZzISbAJIMEUCIQDMRE2ifdUFkiTqhUxAn70DCSA+IHCGdgwlKEatEa83igIgGr7ZKe+P+webuTfU/YOZeP2akcV3bJnSuFnmYGt634UBIQMD1ngcjprG/TU+l5l9kL76CILD4CenKrEq+rpcOR5ahw==", + }, + btcPk: "A9Z4HI6axv01PpeZfZC++giCw+AnpyqxKvq6XDkeWoc=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIFd9W1+yieVJIBCYW5P9qNMlD5ez4vImCH1G2fGv9d8zeWJ7AAAAAAAWABT4b2WQWoQzHeQR0GTpDgCy5krSkQAAAAA=", + stakingTxInclusionProof: { + key: { + index: 182, + hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", + }, + proof: + "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", + }, + slashingTx: + "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", + delegatorSlashingSig: + "8hWpI4ANIZCfwUOH1vJ0la4MS+66WASq/uVzf/XTNnB3fz+T5lP9DPEbpZh9RvcERIMglp34U0gUFa9OOMeMKA==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wGgewcAAAAAACJRIJv737zhksatXOGppeyPe21X1HqGDeeM+aW80NBh2DU/AAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAFoZNa+lz3ZWh798YMtk6HxWkgbCPpCpO/cuBGRVXmPrQAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", + delegatorUnbondingSlashingSig: + "WPWFX3V+jntpE1ItERjK1RHPF/Z2mHW6ImC5iRI81/RQpTc5mFnjyBesBcOTjaB20o88OF1PzdOFbeMPTCiOpA==", + }, + delegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "BIP322", + btcSig: + "Cip0YjFxbHBoa3R5ejZzc2UzbWVxMzZwandqcnNxa3RueTQ1NTNwYXlkZzISbAJIMEUCIQDMRE2ifdUFkiTqhUxAn70DCSA+IHCGdgwlKEatEa83igIgGr7ZKe+P+webuTfU/YOZeP2akcV3bJnSuFnmYGt634UBIQMD1ngcjprG/TU+l5l9kL76CILD4CenKrEq+rpcOR5ahw==", + }, + btcPk: "A9Z4HI6axv01PpeZfZC++giCw+AnpyqxKvq6XDkeWoc=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIFd9W1+yieVJIBCYW5P9qNMlD5ez4vImCH1G2fGv9d8zeWJ7AAAAAAAWABT4b2WQWoQzHeQR0GTpDgCy5krSkQAAAAA=", + slashingTx: + "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", + delegatorSlashingSig: + "8hWpI4ANIZCfwUOH1vJ0la4MS+66WASq/uVzf/XTNnB3fz+T5lP9DPEbpZh9RvcERIMglp34U0gUFa9OOMeMKA==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wGgewcAAAAAACJRIJv737zhksatXOGppeyPe21X1HqGDeeM+aW80NBh2DU/AAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAFoZNa+lz3ZWh798YMtk6HxWkgbCPpCpO/cuBGRVXmPrQAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", + delegatorUnbondingSlashingSig: + "WPWFX3V+jntpE1ItERjK1RHPF/Z2mHW6ImC5iRI81/RQpTc5mFnjyBesBcOTjaB20o88OF1PzdOFbeMPTCiOpA==", + }, + }, + ], + // Legacy + [ + { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "028333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37", + ), + address: "msSV7NptGswtM4k7Qom6f9efJ2rcZQQ8Ho", + }, + { + slashingPsbt: + "70736274ff010070020000000136fed8cea71d15ae6e4feda28f5658dd703d5bd20dfa9616dfb61b87b46578a20000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012b20a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d7414215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac074721ff8a756f465256499df328a3f3caba2cd9255953706a8ac424261d96536fd7901208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + unbondingSlashingPsbt: + "70736274ff0100700200000001d987aa78255e723474e10cfc7ac640432ef138bea619d579d1e28475446acf2c0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012ba07b070000000000225120eb99851e9f7dfdaa1e818a5c4146ee8c6d7683600fa0451e0da3794e1debff564215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac055355230be6141ba692e3ebbc0f553fb05a8160245890ecbceae6704830a9a90fd7901208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", + stakingTxHex: + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d74179627b00000000001976a91482c9274701435286dc0bce950d9648878d466ad688ac00000000", + signedBabylonAddress: + "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", + signType: "ecdsa", + signedSlashingPsbt: + "70736274ff010070020000000136fed8cea71d15ae6e4feda28f5658dd703d5bd20dfa9616dfb61b87b46578a20000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012b20a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d7410108fdff01034086ecbd50fecf3c86748f9504c858ebc3211f8e22cd70e2b96ca0910e0d9664610e84fbaa8447c8431cde3a05e890145c40f039133a98d50b94cabef65dec8e1dfd7801208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac074721ff8a756f465256499df328a3f3caba2cd9255953706a8ac424261d96536000000", + signedUnbondingSlashingPsbt: + "70736274ff0100700200000001d987aa78255e723474e10cfc7ac640432ef138bea619d579d1e28475446acf2c0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012ba07b070000000000225120eb99851e9f7dfdaa1e818a5c4146ee8c6d7683600fa0451e0da3794e1debff560108fdff0103401b884aea88c45e4b557f1159cb7abb6e8d4f2b0af2221d9793e2bb8de801385141f82777662138cd87718a9246fca52e7d7484ec413b0376eaaad1cc28ba9077fd7801208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac055355230be6141ba692e3ebbc0f553fb05a8160245890ecbceae6704830a9a90000000", + postStakingDelegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "ECDSA", + btcSig: + "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", + }, + btcPk: "gzM1jRNYKvGGBzyzrYbDRjDBhtdJBgPEzmD7USIcmjc=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIB7VcMFVVcomNEyNHV7o7YdkooaZgIOQMMaPXdcXJ9dBeWJ7AAAAAAAZdqkUgsknRwFDUobcC86VDZZIh41GataIrAAAAAA=", + stakingTxInclusionProof: { + key: { + index: 182, + hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", + }, + proof: + "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", + }, + slashingTx: + "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", + delegatorSlashingSig: + "huy9UP7PPIZ0j5UEyFjrwyEfjiLNcOK5bKCRDg2WZGEOhPuqhEfIQxzeOgXokBRcQPA5EzqY1QuUyr72XeyOHQ==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wGgewcAAAAAACJRIOuZhR6fff2qHoGKXEFG7oxtdoNgD6BFHg2jeU4d6/9WAAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAHZh6p4JV5yNHThDPx6xkBDLvE4vqYZ1XnR4oR1RGrPLAAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", + delegatorUnbondingSlashingSig: + "G4hK6ojEXktVfxFZy3q7bo1PKwryIh2Xk+K7jegBOFFB+Cd3ZiE4zYdxipJG/KUufXSE7EE7A3bqqtHMKLqQdw==", + }, + delegationMsg: { + stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + pop: { + btcSigType: "ECDSA", + btcSig: + "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", + }, + btcPk: "gzM1jRNYKvGGBzyzrYbDRjDBhtdJBgPEzmD7USIcmjc=", + fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], + stakingTime: 64000, + stakingValue: 500000, + stakingTx: + "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIB7VcMFVVcomNEyNHV7o7YdkooaZgIOQMMaPXdcXJ9dBeWJ7AAAAAAAZdqkUgsknRwFDUobcC86VDZZIh41GataIrAAAAAA=", + slashingTx: + "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", + delegatorSlashingSig: + "huy9UP7PPIZ0j5UEyFjrwyEfjiLNcOK5bKCRDg2WZGEOhPuqhEfIQxzeOgXokBRcQPA5EzqY1QuUyr72XeyOHQ==", + unbondingTime: 1008, + unbondingTx: + "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wGgewcAAAAAACJRIOuZhR6fff2qHoGKXEFG7oxtdoNgD6BFHg2jeU4d6/9WAAAAAA==", + unbondingValue: 490400, + unbondingSlashingTx: + "AgAAAAHZh6p4JV5yNHThDPx6xkBDLvE4vqYZ1XnR4oR1RGrPLAAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", + delegatorUnbondingSlashingSig: + "G4hK6ojEXktVfxFZy3q7bo1PKwryIh2Xk+K7jegBOFFB+Cd3ZiE4zYdxipJG/KUufXSE7EE7A3bqqtHMKLqQdw==", + }, + }, + ], +] as const; + +export const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; + +export const stakingInput = { + stakingAmountSat: 500_000, + finalityProviderPksNoCoordHex: [ + getPublicKeyNoCoord( + "d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76", + ), + ], + stakingTimelock: 64000, +}; + +export const utxos: UTXO[] = [ + { + scriptPubKey: + "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", + txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", + value: 8586757, + vout: 1, + }, +]; + +export const btcTipHeight = 900_000; +export const invalidStartHeightArr = [ + [0, "Babylon BTC tip height cannot be 0"], + [800_000, "Babylon params not found for height 800000"], +] as [number, string][]; + +export const feeRate = 4; + +export const invalidBabylonAddresses = [ + "invalid-babylon-address", + "cosmos1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", + "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8spure", + "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", + "cyqgpk0nlsutlm5ymkfpya30fqntanc8s", +]; + +export const stakingTx = Transaction.fromHex( + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df3379627b0000000000160014f86f65905a84331de411d064e90e00b2e64ad29100000000", +); + +export const inclusionProof = { + blockHashHex: + "000000826cbd5d4da4830d13ed6ed685b0eaaff44903b73f923a1de710735290", + merkle: [ + "6cc416ec8dc5bbc4ba9b600105b0a59bb27052d0c6e1445be17a3a18d6736142", + "87157e864eb7231e1a95e81e5838f49f4edf8b660f516059eb90304efe2b17b9", + "6c76735e36b0844548497d89d07a82fd643bdff131f40d1456bedd1539db357d", + "5a9a68b7bffeada71d23e85456792780760d18cff790b17dc9bd6ea16d3945cc", + "f9ca8817643d23428dbed5158050044fff43f22e6fb28cfbcc72009e4a5552c7", + "f7d4b0610b5c6bfa40c2a312f4e0fd9a0c93aba0134666359171ec17c0e994b0", + "596c9582a2a7dd1dee6d222100e1085fb87956f2f575a07a93a9e9753bfb8a3a", + "c004a4bfb1d8c636b17f82526e59859ae5b34f82bf66c37eb7ee89a9bd3df33c", + "1f3a1322c218e173125752f3bfa95b8c8247bed299bbc136ed048dc462575763", + ], + pos: 182, +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts new file mode 100644 index 0000000000..f1067c95a3 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts @@ -0,0 +1,114 @@ +import { getPublicKeyNoCoord, UTXO } from "../../../../src"; + +export const params = [ + { + version: 0, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 222170, + }, + { + version: 1, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 100000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227443, + }, + { + version: 2, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 5, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227490, + }, +].map((v) => ({ + version: v.version, + covenantNoCoordPks: v.covenant_pks.map((pk) => + String(getPublicKeyNoCoord(pk)), + ), + covenantQuorum: v.covenant_quorum, + minStakingValueSat: v.min_staking_value_sat, + maxStakingValueSat: v.max_staking_value_sat, + minStakingTimeBlocks: v.min_staking_time_blocks, + maxStakingTimeBlocks: v.max_staking_time_blocks, + unbondingTime: v.unbonding_time_blocks, + unbondingFeeSat: v.unbonding_fee_sat, + minCommissionRate: v.min_commission_rate, + maxActiveFinalityProviders: v.max_active_finality_providers, + delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, + slashing: { + slashingPkScriptHex: v.slashing_pk_script, + slashingRate: parseFloat(v.slashing_rate), + minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, + }, + maxStakingAmountSat: v.max_staking_value_sat, + minStakingAmountSat: v.min_staking_value_sat, + btcActivationHeight: v.btc_activation_height, + allowListExpirationHeight: v.allow_list_expiration_height, +})); + +export const stakerInfo = { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", + ), + address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", +}; + +export const utxos: UTXO[] = [ + { + scriptPubKey: + "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", + txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", + value: 8586757, + vout: 1, + }, +]; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts new file mode 100644 index 0000000000..1e6f96eab1 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts @@ -0,0 +1,145 @@ +import { Transaction } from "bitcoinjs-lib"; +import { getPublicKeyNoCoord } from "../../../../src"; + +export const params = [ + { + version: 0, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 222170, + }, + { + version: 1, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 100000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227443, + }, + { + version: 2, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 5, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227490, + }, +].map((v) => ({ + version: v.version, + covenantNoCoordPks: v.covenant_pks.map((pk) => + String(getPublicKeyNoCoord(pk)), + ), + covenantQuorum: v.covenant_quorum, + minStakingValueSat: v.min_staking_value_sat, + maxStakingValueSat: v.max_staking_value_sat, + minStakingTimeBlocks: v.min_staking_time_blocks, + maxStakingTimeBlocks: v.max_staking_time_blocks, + unbondingTime: v.unbonding_time_blocks, + unbondingFeeSat: v.unbonding_fee_sat, + minCommissionRate: v.min_commission_rate, + maxActiveFinalityProviders: v.max_active_finality_providers, + delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, + slashing: { + slashingPkScriptHex: v.slashing_pk_script, + slashingRate: parseFloat(v.slashing_rate), + minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, + }, + maxStakingAmountSat: v.max_staking_value_sat, + minStakingAmountSat: v.min_staking_value_sat, + btcActivationHeight: v.btc_activation_height, + allowListExpirationHeight: v.allow_list_expiration_height, +})); + +export const stakingTx = Transaction.fromHex( + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", +); + +export const stakingInput = { + stakingAmountSat: 11_000, + finalityProviderPksNoCoordHex: [ + getPublicKeyNoCoord( + "02eb83395c33cf784f7dfb90dcc918b5620ddd67fe6617806f079322dc4db2f0", + ), + ], + stakingTimelock: 100, +}; + +export const version = 2; + +export const covenantUnbondingSignatures = [ + { + btcPkHex: + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + sigHex: + "6bcfc07a4b0caa6f047821e6553bad8a4e3a8f134d41619566a8f2b926ea1fa838d4a098eb2ea8516bc1e6f4ea53d23b6af3acc14b9dfb5fbcb57a9756e32606", + }, + { + btcPkHex: + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + sigHex: + "45f23ff78495e8d35b06b504017ff0f57c6d1a48878359675fd51e2e52570910a0e61439761cddcb4a5956a333a943c4937ff13514dd582cdf48435066311f17", + }, + { + btcPkHex: + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + sigHex: + "4cc1246632df302ce78cb374de5d153df107a227d726ec81ec3de49b72cc47d3a0cca3a3ee38d74823390b758f0a451e8b5bb98068e7814ce2664532cba80436", + }, +]; + +export const unbondingPsbt = + "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a36215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141568b200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; + +export const stakerInfo = { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", + ), + address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts new file mode 100644 index 0000000000..93c5e2d691 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts @@ -0,0 +1,129 @@ +import { Transaction } from "bitcoinjs-lib"; +import { getPublicKeyNoCoord } from "../../../../src"; + +export const stakerInfo = { + publicKeyNoCoordHex: getPublicKeyNoCoord( + "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", + ), + address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", +}; + +export const unboundingTx = Transaction.fromHex( + "0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d10000000000ffffffff011c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd31600000000", +); + +export const stakingTx = Transaction.fromHex( + "02000000013ceeae53363582ad438aa20b0e95917b01d8eb8c15b030f5cbfcd90587dfaf720100000000ffffffff021027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf322b810000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", +); + +export const slashingTx = Transaction.fromHex( + "0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d10000000000ffffffff02e803000000000000160014f87283ca2ab20a1ab50cc7cea290f722c9a24574401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac400000000", +); + +export const stakingInput = { + stakingAmountSat: 10_000, + finalityProviderPksNoCoordHex: [ + getPublicKeyNoCoord( + "bb762e89f88a060707371b06fb13a896c1adab058df6a25e35463c14c82eca70", + ), + ], + stakingTimelock: 100, +}; + +export const version = 2; + +export const params = [ + { + version: 0, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 222170, + }, + { + version: 1, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 100000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 20, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227443, + }, + { + version: 2, + covenant_pks: [ + "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", + "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", + "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", + ], + covenant_quorum: 2, + min_staking_value_sat: 10000, + max_staking_value_sat: 1000000000000, + min_staking_time_blocks: 100, + max_staking_time_blocks: 60000, + slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", + min_slashing_tx_fee_sat: 1000, + slashing_rate: "0.100000000000000000", + unbonding_time_blocks: 5, + unbonding_fee_sat: 500, + min_commission_rate: "0.050000000000000000", + max_active_finality_providers: 0, + delegation_creation_base_gas_fee: 1000000, + allow_list_expiration_height: 1440, + btc_activation_height: 227490, + }, +].map((v) => ({ + version: v.version, + covenantNoCoordPks: v.covenant_pks.map((pk) => + String(getPublicKeyNoCoord(pk)), + ), + covenantQuorum: v.covenant_quorum, + minStakingValueSat: v.min_staking_value_sat, + maxStakingValueSat: v.max_staking_value_sat, + minStakingTimeBlocks: v.min_staking_time_blocks, + maxStakingTimeBlocks: v.max_staking_time_blocks, + unbondingTime: v.unbonding_time_blocks, + unbondingFeeSat: v.unbonding_fee_sat, + minCommissionRate: v.min_commission_rate, + maxActiveFinalityProviders: v.max_active_finality_providers, + delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, + slashing: { + slashingPkScriptHex: v.slashing_pk_script, + slashingRate: parseFloat(v.slashing_rate), + minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, + }, + maxStakingAmountSat: v.max_staking_value_sat, + minStakingAmountSat: v.min_staking_value_sat, + btcActivationHeight: v.btc_activation_height, + allowListExpirationHeight: v.allow_list_expiration_height, +})); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts new file mode 100644 index 0000000000..c000c3c261 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts @@ -0,0 +1,81 @@ +import { networks } from "bitcoinjs-lib"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { + btcTipHeight, + feeRate, + invalidStartHeightArr, + stakerInfo, + stakerInfoArr, + stakingInput, + stakingParams, + utxos, +} from "./__mock__/fee"; +import { babylonProvider, btcProvider } from "./__mock__/providers"; + +describe("Staking Manager", () => { + describe("estimateBtcStakingFee", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + stakingParams, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + }); + + it.each(invalidStartHeightArr)( + "should validate babylonBtcTipHeight", + async (btcTipHeight, errorMessage) => { + try { + await manager.estimateBtcStakingFee( + stakerInfo, + btcTipHeight, + stakingInput, + utxos, + feeRate, + ); + } catch (e: any) { + expect(e.message).toMatch(errorMessage); + } + }, + ); + + it("should validate babylonBtcTipHeight", async () => { + const btcTipHeight = 100; + + try { + await manager.estimateBtcStakingFee( + stakerInfo, + btcTipHeight, + stakingInput, + utxos, + feeRate, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for height ${btcTipHeight}`, + ); + } + }); + + it.each(stakerInfoArr)("should return valid tx fee", async (stakerInfo) => { + const txFee = await manager.estimateBtcStakingFee( + stakerInfo, + btcTipHeight, + stakingInput, + utxos, + feeRate, + ); + + expect(txFee).toEqual(620); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts new file mode 100644 index 0000000000..57bf0c5618 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts @@ -0,0 +1,33 @@ +import { networks } from "bitcoinjs-lib"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { params } from "./__mock__/staking"; + +describe("Staking Manager", () => { + describe("Initialization", () => { + it("should succesfully initialize Staking Manager", () => { + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + ); + + expect(manager).toBeDefined(); + }); + + it("should throw an init error", () => { + expect( + () => + new BabylonBtcStakingManager( + networks.bitcoin, + [], + btcProvider, + babylonProvider, + ), + ).toThrow("No staking parameters provided"); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts new file mode 100644 index 0000000000..b6f9933173 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts @@ -0,0 +1,226 @@ +import { networks } from "bitcoinjs-lib"; +import { sha256 } from "bitcoinjs-lib/src/crypto"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; +import { STAKING_MODULE_ADDRESS } from "../../../src/constants/staking"; + +import { babylonProvider, btcProvider, mockChainId } from "./__mock__/providers"; +import { params, stakerInfo } from "./__mock__/staking"; +import { babylonAddress } from "./__mock__/fee"; + +describe("Staking Manager - POP Integration", () => { + const mockBech32Address = babylonAddress; + const mockBtcAddress = stakerInfo.address; + + beforeEach(() => { + jest.clearAllMocks(); + btcProvider.signMessage.mockResolvedValue("mocked-signature"); + babylonProvider.getChainId.mockResolvedValue(mockChainId); + }); + + describe("Legacy POP Format (Below Upgrade Height)", () => { + it("should use legacy format when height is below upgrade height", async () => { + const mockGetCurrentHeight = jest.fn().mockResolvedValue(100); + babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); + + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + undefined, + { + pop: upgradeConfig, + }, + ); + + await manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockBtcAddress, + ); + + // Should sign just the bech32 address (legacy format) + expect(btcProvider.signMessage).toHaveBeenCalledWith( + mockBech32Address, + "ecdsa", + ); + expect(mockGetCurrentHeight).toHaveBeenCalled(); + }); + + it("should use legacy format when no upgrade options provided and optional babylon provider methods are not provided", async () => { + babylonProvider.getCurrentHeight.mockResolvedValue(undefined); + babylonProvider.getChainId.mockResolvedValue(undefined); + + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + ); + + await manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockBtcAddress, + ); + + // Should sign just the bech32 address (legacy format) + expect(btcProvider.signMessage).toHaveBeenCalledWith( + mockBech32Address, + "ecdsa", + ); + }); + }); + + describe("New POP Format (Above Upgrade Height)", () => { + it("should use new format when height is above upgrade height", async () => { + const mockGetCurrentHeight = jest.fn().mockResolvedValue(300); + babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); + + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + undefined, + { + pop: upgradeConfig, + }, + ); + + await manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockBtcAddress, + ); + + // Calculate expected message with context hash + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(btcProvider.signMessage).toHaveBeenCalledWith( + expectedMessage, + "ecdsa", + ); + expect(mockGetCurrentHeight).toHaveBeenCalled(); + }); + + it("should use new format when popContextUpgradeHeight is 0 (always use new format)", async () => { + const mockGetCurrentHeight = jest.fn().mockResolvedValue(100); + babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); + + const upgradeConfig = { + upgradeHeight: 0, + version: 0, + }; + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + undefined, + { + pop: upgradeConfig, + }, + ); + + await manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockBtcAddress, + ); + + // Calculate expected message with context hash + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(btcProvider.signMessage).toHaveBeenCalledWith( + expectedMessage, + "ecdsa", + ); + }); + }); + + describe("Error Handling", () => { + it("should throw error when height detection fails", async () => { + const mockGetCurrentHeight = jest + .fn() + .mockRejectedValue(new Error("Network error")); + babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + undefined, + { + pop: upgradeConfig, + }, + ); + + await expect( + manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockBtcAddress, + ), + ).rejects.toThrow("Network error"); + }); + }); + + describe("BIP322 Support", () => { + it("should use BIP322 signature for taproot addresses with new format", async () => { + const mockTaprootAddress = + "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"; + const mockGetCurrentHeight = jest.fn().mockResolvedValue(300); + babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + + const manager = new BabylonBtcStakingManager( + networks.bitcoin, + params, + btcProvider, + babylonProvider, + undefined, + { + pop: upgradeConfig, + }, + ); + + await manager.createProofOfPossession( + "delegation:create", + mockBech32Address, + mockTaprootAddress, + ); + + // Calculate expected message with context hash + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(btcProvider.signMessage).toHaveBeenCalledWith( + expectedMessage, + "bip322-simple", + ); + }); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts new file mode 100644 index 0000000000..75a1a2ee59 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts @@ -0,0 +1,211 @@ +import { networks, Transaction } from "bitcoinjs-lib"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { ContractId } from "../../../src/types/contract"; +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { + babylonAddress, + btcTipHeight, + inclusionProof, + invalidBabylonAddresses, + invalidStartHeightArr, + params, + stakerInfo, + stakerInfoArr, + stakingInput, + stakingTx, +} from "./__mock__/registration"; +import { ActionName } from "../../../src/types/action"; + +describe("Staking Manager", () => { + describe("postStakeRegistrationBabylonTransaction", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + params, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + btcProvider.signMessage.mockReset(); + babylonProvider.signTransaction.mockReset(); + }); + + it.each(invalidStartHeightArr)( + "should validate babylonBtcTipHeight %s", + async (btcTipHeight) => { + try { + await manager.postStakeRegistrationBabylonTransaction( + stakerInfo, + stakingTx, + btcTipHeight, + stakingInput, + inclusionProof, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for height ${btcTipHeight}`, + ); + } + }, + ); + + it.each(invalidBabylonAddresses)( + "should validate babylon address", + async (babylonAddress) => { + try { + await manager.postStakeRegistrationBabylonTransaction( + stakerInfo, + stakingTx, + btcTipHeight, + stakingInput, + inclusionProof, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch("Invalid Babylon address"); + } + }, + ); + + it("should validate tx output", async () => { + const tx = { + ...stakingTx, + outs: [], + } as any; + + try { + await manager.postStakeRegistrationBabylonTransaction( + stakerInfo, + tx, + btcTipHeight, + stakingInput, + inclusionProof, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch(/Matching output not found for address:/); + } + }); + + it.each(stakerInfoArr)( + "should create valid pre stake registration tx", + async ( + stakerInfo, + { + slashingPsbt, + unbondingSlashingPsbt, + signedSlashingPsbt, + signedUnbondingSlashingPsbt, + signedBabylonAddress, + stakingTxHex, + postStakingDelegationMsg, + signType, + }, + ) => { + const version = 4; + + btcProvider.signPsbt + .mockResolvedValueOnce(signedSlashingPsbt) + .mockResolvedValueOnce(signedUnbondingSlashingPsbt); + btcProvider.signMessage.mockResolvedValueOnce(signedBabylonAddress); + + await manager.postStakeRegistrationBabylonTransaction( + stakerInfo, + Transaction.fromHex(stakingTxHex), + btcTipHeight, + stakingInput, + inclusionProof, + babylonAddress, + ); + + expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { + contracts: [ + { + id: ContractId.STAKING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + minUnbondingTime: params[version].unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + }, + }, + { + id: ContractId.SLASHING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + unbondingTimeBlocks: params[version].unbondingTime, + slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, + }, + }, + { + id: ContractId.SLASHING_BURN, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, + }, + }); + expect(btcProvider.signPsbt).toHaveBeenCalledWith( + unbondingSlashingPsbt, + { + contracts: [ + { + id: ContractId.UNBONDING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + unbondingTimeBlocks: params[version].unbondingTime, + unbondingFeeSat: params[version].unbondingFeeSat, + }, + }, + { + id: ContractId.SLASHING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + unbondingTimeBlocks: params[version].unbondingTime, + slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, + }, + }, + { + id: ContractId.SLASHING_BURN, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, + }, + }, + ); + expect(btcProvider.signMessage).toHaveBeenCalledWith( + babylonAddress, + signType, + ); + expect( + btcstakingtx.MsgCreateBTCDelegation.toJSON( + babylonProvider.signTransaction.mock.calls[0][0].value, + ), + ).toEqual(postStakingDelegationMsg); + }, + ); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts new file mode 100644 index 0000000000..62d5adeffb --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts @@ -0,0 +1,228 @@ +import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { networks } from "bitcoinjs-lib"; + +import { type UTXO } from "../../../src"; +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { ContractId } from "../../../src/types/contract"; +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { + babylonAddress, + btcTipHeight, + feeRate, + invalidBabylonAddresses, + invalidStartHeightArr, + params, + stakerInfo, + stakerInfoArr, + stakingInput, + utxos, +} from "./__mock__/registration"; +import { ActionName } from "../../../src/types/action"; + +describe("Staking Manager", () => { + describe("preStakeRegistrationBabylonTransaction", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + params, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + btcProvider.signMessage.mockReset(); + babylonProvider.signTransaction.mockReset(); + }); + + it.each(invalidStartHeightArr)( + "should validate babylonBtcTipHeight", + async (btcTipHeight, errorMessage) => { + try { + await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + btcTipHeight, + utxos, + feeRate, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch(errorMessage); + } + }, + ); + + it("should validate input UTXOs", async () => { + const utxos: UTXO[] = []; + + try { + await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + btcTipHeight, + utxos, + feeRate, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch("No input UTXOs provided"); + } + }); + + it.each(invalidBabylonAddresses)( + "should validate babylon address", + async (babylonAddress) => { + try { + await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + btcTipHeight, + utxos, + feeRate, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch("Invalid Babylon address"); + } + }, + ); + + it("should validate babylon params", async () => { + const btcTipHeight = 100; + + try { + await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + btcTipHeight, + utxos, + feeRate, + babylonAddress, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for height ${btcTipHeight}`, + ); + } + }); + + it.each(stakerInfoArr)( + "should create valid pre stake registration tx", + async ( + stakerInfo, + { + signedSlashingPsbt, + signedUnbondingSlashingPsbt, + signedBabylonAddress, + slashingPsbt, + unbondingSlashingPsbt, + delegationMsg, + stakingTxHex, + signType, + }, + ) => { + const version = 4; + + btcProvider.signPsbt + .mockResolvedValueOnce(signedSlashingPsbt) + .mockResolvedValueOnce(signedUnbondingSlashingPsbt); + btcProvider.signMessage.mockResolvedValueOnce(signedBabylonAddress); + + const { stakingTx } = + await manager.preStakeRegistrationBabylonTransaction( + stakerInfo, + stakingInput, + btcTipHeight, + utxos, + feeRate, + babylonAddress, + ); + + expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { + contracts: [ + { + id: ContractId.STAKING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + minUnbondingTime: params[version].unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + }, + }, + { + id: ContractId.SLASHING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + unbondingTimeBlocks: params[version].unbondingTime, + slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, + }, + }, + { + id: ContractId.SLASHING_BURN, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, + }, + }); + expect(btcProvider.signPsbt).toHaveBeenCalledWith( + unbondingSlashingPsbt, + { + contracts: [ + { + id: ContractId.UNBONDING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + unbondingTimeBlocks: params[version].unbondingTime, + unbondingFeeSat: params[version].unbondingFeeSat, + }, + }, + { + id: ContractId.SLASHING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + unbondingTimeBlocks: params[version].unbondingTime, + slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, + }, + }, + { + id: ContractId.SLASHING_BURN, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, + }, + }, + ); + expect(btcProvider.signMessage).toHaveBeenCalledWith( + babylonAddress, + signType, + ); + expect( + btcstakingtx.MsgCreateBTCDelegation.toJSON( + babylonProvider.signTransaction.mock.calls[0][0].value, + ), + ).toEqual(delegationMsg); + expect(stakingTx.toHex()).toBe(stakingTxHex); + }, + ); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts new file mode 100644 index 0000000000..3f7663f1b7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts @@ -0,0 +1,112 @@ +import { networks, Psbt, Transaction } from "bitcoinjs-lib"; + +import { getPublicKeyNoCoord, type UTXO } from "../../../src"; +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { params, stakerInfo, utxos } from "./__mock__/staking"; +import { ContractId } from "../../../src/types/contract"; +import { ActionName } from "../../../src/types/action"; + +const unsignedStakingTx = Transaction.fromHex( + "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", +); +const stakingInput = { + stakingAmountSat: 11_000, + finalityProviderPksNoCoordHex: [ + getPublicKeyNoCoord( + "02eb83395c33cf784f7dfb90dcc918b5620ddd67fe6617806f079322dc4db2f0", + ), + ], + stakingTimelock: 100, +}; +const version = 2; + +describe("Staking Manager", () => { + describe("createSignedBtcStakingTransaction", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + params, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + }); + + it("should validate version params", async () => { + const version = 5; + + try { + await manager.createSignedBtcStakingTransaction( + stakerInfo, + stakingInput, + unsignedStakingTx, + utxos, + version, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should validate input utxos", async () => { + const utxos: UTXO[] = []; + + try { + await manager.createSignedBtcStakingTransaction( + stakerInfo, + stakingInput, + unsignedStakingTx, + utxos, + version, + ); + } catch (e: any) { + expect(e.message).toMatch("No input UTXOs provided"); + } + }); + + it("should sign staking tx", async () => { + const unsignedTx = + "70736274ff0100890200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b0506830000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce0117200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c000000"; + const signedTx = + "70736274ff0100890200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b0506830000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce01084201404039aac248eb40aaab21fd58556ad7d81e177df5119c7225b1e881fc6c64c106d2ba95865895050df5fe64a86f81f3273687056f97fd506c792ebce281d8dbb0000000"; + btcProvider.signPsbt.mockResolvedValueOnce(signedTx); + + const tx = await manager.createSignedBtcStakingTransaction( + stakerInfo, + stakingInput, + unsignedStakingTx, + utxos, + version, + ); + + expect(btcProvider.signPsbt).toHaveBeenLastCalledWith(unsignedTx, { + contracts: [ + { + id: ContractId.STAKING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + minUnbondingTime: params[version].unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_STAKING_TRANSACTION, + }, + }); + expect(tx).toEqual(Psbt.fromHex(signedTx).extractTransaction()); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts new file mode 100644 index 0000000000..269527edb1 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts @@ -0,0 +1,172 @@ +import { networks, Psbt, Transaction } from "bitcoinjs-lib"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { ContractId } from "../../../src/types/contract"; +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { + covenantUnbondingSignatures, + params, + stakerInfo, + stakingInput, + stakingTx, + unbondingPsbt, + version, +} from "./__mock__/unbonding"; +import { ActionName } from "../../../src/types/action"; + +describe("Staking Manager", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + params, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + }); + + describe("createPartialSignedBtcUnbondingTransaction", () => { + it("should validate version params", async () => { + const version = 5; + + try { + await manager.createPartialSignedBtcUnbondingTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should create partial signed unbonding tx", async () => { + const signedUnbondingTx = + "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; + btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); + + const { transaction, fee } = + await manager.createPartialSignedBtcUnbondingTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + ); + + expect(btcProvider.signPsbt).toHaveBeenLastCalledWith(unbondingPsbt, { + contracts: [ + { + id: ContractId.STAKING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + minUnbondingTime: params[version].unbondingTime, + stakingDuration: stakingInput.stakingTimelock, + }, + }, + { + id: ContractId.UNBONDING, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + finalityProviders: stakingInput.finalityProviderPksNoCoordHex, + covenantPks: params[version].covenantNoCoordPks, + covenantThreshold: params[version].covenantQuorum, + unbondingTimeBlocks: params[version].unbondingTime, + unbondingFeeSat: params[version].unbondingFeeSat, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_UNBONDING_TRANSACTION, + }, + }); + expect(transaction).toEqual( + Psbt.fromHex(signedUnbondingTx).extractTransaction(), + ); + expect(fee).toEqual(500); + }); + }); + + describe("createSignedBtcUnbondingTransaction", () => { + it("should validate version params", async () => { + const version = 5; + const unbondingTx = Transaction.fromHex( + "02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b94400000000", + ); + + try { + await manager.createSignedBtcUnbondingTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + unbondingTx, + covenantUnbondingSignatures, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should validate unbonding tx", async () => { + const signedUnbondingTx = + "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; + const unbondingTx = Transaction.fromHex( + "02000000013ceeae53363582ad438aa20b0e95917b01d8eb8c15b030f5cbfcd90587dfaf720000000000ffffffff01ac84010000000000225120453e6f3b8f487fb51b9e598e08eb83febb6e6d0d749c883b1696bbe1a269722600000000", + ); + btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); + + try { + await manager.createSignedBtcUnbondingTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + unbondingTx, + covenantUnbondingSignatures, + ); + } catch (e: any) { + expect(e.message).toMatch( + "Unbonding transaction hash does not match the computed hash", + ); + } + }); + + it("should validate version params", async () => { + const signedUnbondingTx = + "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; + const unbondingTx = Transaction.fromHex( + "02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b94400000000", + ); + btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); + + const { transaction, fee } = + await manager.createSignedBtcUnbondingTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + unbondingTx, + covenantUnbondingSignatures, + ); + + expect(fee).toEqual(500); + expect(transaction.toHex()).toBe( + "020000000001011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944064045f23ff78495e8d35b06b504017ff0f57c6d1a48878359675fd51e2e52570910a0e61439761cddcb4a5956a333a943c4937ff13514dd582cdf48435066311f17406bcfc07a4b0caa6f047821e6553bad8a4e3a8f134d41619566a8f2b926ea1fa838d4a098eb2ea8516bc1e6f4ea53d23b6af3acc14b9dfb5fbcb57a9756e326060040beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab1415600000000", + ); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts new file mode 100644 index 0000000000..648a7a8946 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts @@ -0,0 +1,208 @@ +import { networks, Psbt } from "bitcoinjs-lib"; + +import { BabylonBtcStakingManager } from "../../../src/staking/manager"; + +import { babylonProvider, btcProvider } from "./__mock__/providers"; +import { + params, + slashingTx, + stakerInfo, + stakingInput, + stakingTx, + unboundingTx, + version, +} from "./__mock__/withdrawal"; +import { ContractId } from "../../../src/types/contract"; +import { ActionName } from "../../../src/types/action"; + +describe("Staking Manager", () => { + describe("Create Withdrawal Transaction", () => { + let manager: BabylonBtcStakingManager; + + beforeEach(() => { + manager = new BabylonBtcStakingManager( + networks.testnet, + params, + btcProvider, + babylonProvider, + ); + }); + + afterEach(() => { + btcProvider.signPsbt.mockReset(); + }); + + // Early Unbonded + describe("createSignedBtcWithdrawEarlyUnbondedTransaction", () => { + it("should validate version params", async () => { + const version = 5; + + try { + await manager.createSignedBtcWithdrawEarlyUnbondedTransaction( + stakerInfo, + stakingInput, + version, + unboundingTx, + 4, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should create withdrawal tx", async () => { + const unbondingPsbt = + "70736274ff01005e020000000157b41412aa878b560867e9e97b52247555a7016a679e59dc5084c728ecbe7091000000000005000000011823000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd3164215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd25200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; + const signedUnbondingPsbt = + "70736274ff01005e020000000157b41412aa878b560867e9e97b52247555a7016a679e59dc5084c728ecbe7091000000000005000000011823000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd3160108a903405ff651c35d926db0c6a93e8852cb6d3d59e0355a2c6f09aaef817e406887fe9e8f4f05b64e0efab47ab8670e8b8891e8df60a43eb402cc63197a224890c3ed1f24200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b241c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd0000"; + btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingPsbt); + + const { transaction, fee } = + await manager.createSignedBtcWithdrawEarlyUnbondedTransaction( + stakerInfo, + stakingInput, + version, + unboundingTx, + 4, + ); + + expect(btcProvider.signPsbt).toHaveBeenCalledWith(unbondingPsbt, { + contracts: [ + { + id: ContractId.WITHDRAW, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + timelockBlocks: params[version].unbondingTime, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, + }, + }); + expect(transaction.toHex()).toBe( + Psbt.fromHex(signedUnbondingPsbt).extractTransaction().toHex(), + ); + expect(fee).toEqual(516); + }); + }); + + // Staking Expired + describe("createSignedBtcWithdrawStakingExpiredTransaction", () => { + it("should validate version params", async () => { + const version = 5; + + try { + await manager.createSignedBtcWithdrawStakingExpiredTransaction( + stakerInfo, + stakingInput, + version, + stakingTx, + 4, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should create withdrawal tx", async () => { + const withdrawPsbt = + "70736274ff01005e0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d1000000000064000000010c25000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf6215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08317b993160b148500d8fc1f6520c7f8ead4ae9537268552dcc20238fb6bb3f5802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd26200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad0164b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; + const signedwithdrawPsbt = + "70736274ff01005e0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d1000000000064000000010c25000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf0108ca0340726cd5ac48fe5728720d4179a68d4fe59076762f980626a82bc83c9d89c750364b4cd9d0a5acbf9259e2fe3977c7becc5e16b24dde88fe2792095b23ada1ffdf25200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad0164b261c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08317b993160b148500d8fc1f6520c7f8ead4ae9537268552dcc20238fb6bb3f5802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd0000"; + btcProvider.signPsbt.mockResolvedValueOnce(signedwithdrawPsbt); + + const { transaction, fee } = + await manager.createSignedBtcWithdrawStakingExpiredTransaction( + stakerInfo, + stakingInput, + version, + + stakingTx, + 4, + ); + + expect(btcProvider.signPsbt).toHaveBeenCalledWith(withdrawPsbt, { + contracts: [ + { + id: ContractId.WITHDRAW, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + timelockBlocks: stakingInput.stakingTimelock, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, + }, + }); + expect(transaction.toHex()).toBe( + Psbt.fromHex(signedwithdrawPsbt).extractTransaction().toHex(), + ); + expect(fee).toEqual(516); + }); + }); + + // Slashed + describe("createSignedBtcWithdrawSlashingTransaction", () => { + it("should validate version params", async () => { + const version = 5; + + try { + await manager.createSignedBtcWithdrawSlashingTransaction( + stakerInfo, + stakingInput, + version, + slashingTx, + 2, + ); + } catch (e: any) { + expect(e.message).toMatch( + `Babylon params not found for version ${version}`, + ); + } + }); + + it("should create withdrawal tx", async () => { + const slashingPsbt = + "70736274ff01005e02000000019756644d209e088a6d0f29a20bd5bcc60496a6d2302df0e29019f3f4f572cc6101000000000500000001201e000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac42215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac025200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; + const signedSlashingPsbt = + "70736274ff01005e02000000019756644d209e088a6d0f29a20bd5bcc60496a6d2302df0e29019f3f4f572cc6101000000000500000001201e000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac401088903402692ff631a6a6ce5dfdf33ed2d3e9573d7752cbc4ad98f8d117417c24d7b2a4f98458aa7c4e95f59bcf1c41b80e4a433867f3dd4540ad121f0a546b7945a5fec24200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b221c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; + btcProvider.signPsbt.mockResolvedValueOnce(signedSlashingPsbt); + + const { transaction, fee } = + await manager.createSignedBtcWithdrawSlashingTransaction( + stakerInfo, + stakingInput, + version, + slashingTx, + 2, + ); + + expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { + contracts: [ + { + id: ContractId.WITHDRAW, + params: { + stakerPk: stakerInfo.publicKeyNoCoordHex, + timelockBlocks: params[version].unbondingTime, + }, + }, + ], + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, + }, + }); + + expect(transaction.toHex()).toBe( + Psbt.fromHex(signedSlashingPsbt).extractTransaction().toHex(), + ); + expect(fee).toEqual(288); + }); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts new file mode 100644 index 0000000000..9be9f6a20f --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts @@ -0,0 +1,162 @@ +import { address, networks } from "bitcoinjs-lib"; +import { ObservableStaking, transactionIdToHash } from "../../../src"; +import * as observableStakingScriptData from "../../../src/staking/observable/observableStakingScript"; +import { ObservableVersionedStakingParams } from "../../../src/types/params"; +import { UTXO } from "../../../src/types/UTXO"; +import { StakingError, StakingErrorCode } from "../../../src/error"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import * as stakingUtils from "../../../src/utils/staking/validation"; +import * as staking from "../../../src/staking/transactions"; +import { ObservableStakingDatagen } from "../../helper/datagen/observable"; + +// TODO: To be removed +describe.each([networks.bitcoin, networks.testnet])("Observal - Create staking transaction", (network) => { + const dataGenerator= new ObservableStakingDatagen(network) + const networkName = network === networks.bitcoin ? "mainnet" : "testnet"; + + let stakerInfo: { address: string, publicKeyNoCoordHex: string, publicKeyWithCoord: string }; + let finalityProviderPksNoCoord: string[]; + let params: ObservableVersionedStakingParams; + let timelock: number; + let utxos: UTXO[]; + const feeRate = 1; + let observableStaking: ObservableStaking; + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + + const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); + const { address, scriptPubKey } = dataGenerator.getAddressAndScriptPubKey( + publicKey, + ).taproot; + stakerInfo = { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + publicKeyWithCoord: publicKey, + }; + finalityProviderPksNoCoord = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(1); + params = dataGenerator.generateStakingParams(true); + timelock = dataGenerator.generateRandomTimelock(params); + utxos = dataGenerator.generateRandomUTXOs( + params.maxStakingAmountSat * dataGenerator.getRandomIntegerBetween(1, 100), + dataGenerator.getRandomIntegerBetween(1, 10), + scriptPubKey, + ); + observableStaking = new ObservableStaking( + network, stakerInfo, + params, finalityProviderPksNoCoord, timelock, + ); + }); + + it(`${networkName} should throw an error if input data validation failed`, async () => { + jest.spyOn(stakingUtils, "validateStakingTxInputData").mockImplementation(() => { + throw new StakingError(StakingErrorCode.INVALID_INPUT, "some error"); + }); + + expect(() => observableStaking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.INVALID_INPUT, "some error") + ); + }); + + it(`${networkName} should throw an error if fail to build scripts`, async () => { + jest.spyOn(observableStakingScriptData, "ObservableStakingScriptData").mockImplementation(() => { + throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error"); + }); + expect(() => observableStaking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error") + ); + }); + + it(`${networkName} should throw an error if fail to build staking tx`, async () => { + jest.spyOn(staking, "stakingTransaction").mockImplementation(() => { + throw new Error("fail to build staking tx"); + }); + + expect(() => observableStaking.createStakingTransaction( + params.minStakingAmountSat, + utxos, + feeRate, + )).toThrow( + new StakingError(StakingErrorCode.BUILD_TRANSACTION_FAILURE, "fail to build staking tx") + ); + }); + + it(`${networkName} should successfully create a observable staking transaction`, async () => { + const amount = dataGenerator.getRandomIntegerBetween( + params.minStakingAmountSat, params.maxStakingAmountSat, + ); + const { transaction, fee} = observableStaking.createStakingTransaction( + amount, + utxos, + feeRate, + ); + + expect(transaction).toBeDefined(); + expect(fee).toBeGreaterThan(0); + + const psbt = observableStaking.toStakingPsbt(transaction, utxos); + // Check the inputs + expect(transaction.ins.length).toBeGreaterThan(0); + + // Check the outputs + expect(transaction.outs.length).toBeGreaterThanOrEqual(1); + // build the psbt input amount from psbt.data.inputs + let psbtInputAmount = 0; + for (let i = 0; i < psbt.data.inputs.length; i++) { + const newValue = psbt.data.inputs[i].witnessUtxo?.value || 0; + psbtInputAmount += newValue; + } + const psbtChangeAmount = psbtInputAmount - amount - fee; + + let txInputAmount = 0; + for (let i = 0; i < transaction.ins.length; i++) { + const input = transaction.ins[i]; + const utxo = utxos.find(u => + transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") + && u.vout === input.index, + ); + txInputAmount += utxo?.value || 0; + } + const changeAmount = txInputAmount - amount - fee; + expect(txInputAmount).toBeGreaterThanOrEqual(amount + fee); + if (changeAmount > BTC_DUST_SAT) { + expect(transaction.outs[transaction.outs.length - 1].value).toEqual(changeAmount); + expect(transaction.outs[transaction.outs.length - 1].script) + .toEqual(address.toOutputScript(stakerInfo.address, network)); + } + expect(transaction.outs[0].value).toEqual(amount); + expect(psbt.txOutputs[0].value).toEqual(amount); + + + // Check the psbt properties + expect(transaction.locktime).toBe(params.btcActivationHeight - 1); + expect(transaction.version).toBe(2); + transaction.ins.map((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + + // Check the data embed script(OP_RETURN) + const scripts = observableStaking.buildScripts(); + const dataEmbedOutput = transaction.outs.find((output) => + output.script.equals(scripts.dataEmbedScript), + ); + expect(dataEmbedOutput).toBeDefined(); + + expect(psbtChangeAmount).toEqual(changeAmount); + expect(psbtInputAmount).toEqual(txInputAmount); + // lock time and version are the same between psbt and transaction + expect(psbt.locktime).toEqual(transaction.locktime); + expect(psbt.version).toEqual(transaction.version); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts new file mode 100644 index 0000000000..daec543180 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts @@ -0,0 +1,152 @@ +import { opcodes, script } from "bitcoinjs-lib"; +import { ObservableStakingScriptData } from "../../../src"; + +describe("observableStakingScript", () => { + const pk1 = Buffer.from( + "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd068", + "hex", + ); + const pk2 = Buffer.from( + "f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8e", + "hex", + ); + const pk3 = Buffer.from( + "17921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4", + "hex", + ); + const pk4 = Buffer.from( + "76d1ae01f8fb6bf30108731c884cddcf57ef6eef2d9d9559e130894e0e40c62c", + "hex", + ); + const pk5 = Buffer.from( + "49766ccd9e3cd94343e2040474a77fb37cdfd30530d05f9f1e96ae1e2102c86e", + "hex", + ); + const pk6 = Buffer.from( + "063deb187a4bf11c114cf825a4726e4c2c35fea5c4c44a20ff08a30a752ec7e0", + "hex", + ); + const invalidPk = Buffer.from( + "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd0", + "hex", + ); + const stakingTimeLock = 65535; + const unbondingTimeLock = 1000; + const magicBytes = Buffer.from("62626234", "hex"); + + describe("Error path", () => { + it("should throw if more than one finality providers when building data embed script", () => { + const script = new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2, pk6], // More than one FP Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + magicBytes + ); + expect(() => + script.buildDataEmbedScript() + ).toThrow("Only a single finality provider key is supported"); + }); + + it("should fail if the magic bytes are below 4 in length", () => { + expect( + () => + new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + Buffer.from("aaaaaa", "hex"), + ), + ).toThrow("Invalid script data provided"); + }); + it("should fail if the magic bytes are above 4 in length", () => { + expect( + () => + new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + Buffer.from("aaaaaaaaaa", "hex"), + ), + ).toThrow("Invalid script data provided"); + }); + }); + + describe("Happy path", () => { + it("should succeed with valid input data", () => { + const scriptData = new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + magicBytes, + ); + expect(scriptData).toBeInstanceOf(ObservableStakingScriptData); + }); + + it("should build valid data embed script", () => { + const scriptData = new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + magicBytes, + ); + const dataEmbedScript = scriptData.buildDataEmbedScript(); + const decompiled = script.decompile(dataEmbedScript); + expect(decompiled).toEqual([ + opcodes.OP_RETURN, + Buffer.concat([ + magicBytes, + Buffer.from([0]), // Version byte + pk1, + pk2, + Buffer.from([stakingTimeLock >> 8, stakingTimeLock & 0xff]), // Staking timelock in big endian + ]), + ]); + }); + + it("should build valid staking scripts", () => { + const scriptData = new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + magicBytes, + ); + const scripts = scriptData.buildScripts(); + expect(scripts).toHaveProperty("timelockScript"); + expect(scripts).toHaveProperty("unbondingScript"); + expect(scripts).toHaveProperty("slashingScript"); + expect(scripts).toHaveProperty("unbondingTimelockScript"); + expect(scripts).toHaveProperty("dataEmbedScript"); + }); + + it("should validate correctly with valid input data", () => { + const scriptData = new ObservableStakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + magicBytes, + ); + expect(scriptData.validate()).toBe(true); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts new file mode 100644 index 0000000000..b8a433c938 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts @@ -0,0 +1,78 @@ +import { networks } from 'bitcoinjs-lib'; +import { ObservableStaking } from '../../../src/staking/observable'; +import { ObservableStakingDatagen } from '../../helper/datagen/observable'; + +// TODO: To be removed +describe.each([networks.bitcoin, networks.testnet])("Observable", (network) => { + const networkName = network === networks.bitcoin ? "mainnet" : "testnet"; + const dataGenerator = new ObservableStakingDatagen(network); + + describe(`${networkName} validateParams`, () => { + const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); + const { address } = dataGenerator.getAddressAndScriptPubKey( + publicKey, + ).taproot; + const params = dataGenerator.generateStakingParams(true); + + const stakerInfo = { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + publicKeyWithCoord: publicKey, + }; + const validParams = dataGenerator.generateStakingParams(); + + const finalityProviderPksNoCoordHex = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(1); + + it('should pass with valid parameters', () => { + expect(() => new ObservableStaking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(params), + )).not.toThrow(); + }); + + it('should throw an error if no tag', () => { + const params = { ...validParams, tag: "" }; + + expect(() => new ObservableStaking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(params), + )).toThrow( + "Observable staking parameters must include tag" + ); + }); + + it('should throw an error if no btcActivationHeight', () => { + const params = { ...validParams, btcActivationHeight: 0 }; + + expect(() => new ObservableStaking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(params), + )).toThrow( + "Observable staking parameters must include a positive activation height" + ); + }); + + it('should throw an error if number of finality provider public keys is not 1', () => { + const finalityProviderPksNoCoordHex = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(2); + + expect(() => new ObservableStaking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(params), + )).toThrow( + "Observable staking requires exactly one finality provider public key" + ); + }); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts new file mode 100644 index 0000000000..a6c525648f --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts @@ -0,0 +1,464 @@ +import { Transaction } from "bitcoinjs-lib"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { stakingExpansionTransaction } from "../../../src/staking/transactions"; +import { transactionIdToHash } from "../../../src/utils/btc"; +import { testingNetworks } from "../../helper"; +import { stakingExpansionPsbt } from "../../../src/staking/psbt"; +import { internalPubkey } from "../../../src/constants/internalPubkey"; + +describe.each(testingNetworks)("Transactions - ", ( + {network, networkName, datagen} +) => { + describe.each(Object.values(datagen))("stakingExpansionPsbt", ( + dataGenerator + ) => { + const feeRate = 1; + const stakerKeyPair = dataGenerator.generateRandomKeyPair(); + const { + stakingTx: previousStakingTx, + stakingAmountSat, + stakerInfo, + scriptPubKey, + stakingInstance: previousStakingInstance, + } = dataGenerator.generateRandomStakingTransaction( + network, + feeRate, + stakerKeyPair, + ); + + const previousStakingScript = previousStakingInstance.buildScripts(); + const utxos = dataGenerator.generateRandomUTXOs( + 10000, // Big enough to cover the fees + dataGenerator.getRandomIntegerBetween(1, 10), + scriptPubKey, + ); + + describe("Error path", () => { + it(`${networkName} - should throw an error if the public key is invalid`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + const invalidPublicKey = Buffer.from("invalidPublicKey", "hex"); + expect(() => + stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + invalidPublicKey, + ), + ).toThrow("Invalid public key"); + }); + + it(`${networkName} - should throw an error if previous staking output not found`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + expect(() => + stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 999, // Invalid output index + }, + utxos, + previousStakingScript, + ), + ).toThrow("Previous staking output not found"); + }); + + it(`${networkName} - should throw an error if previous staking output is not P2TR`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Create a modified previous staking transaction with non-P2TR output + const modifiedPreviousTx = Transaction.fromBuffer(previousStakingTx.toBuffer()); + const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + modifiedPreviousTx.outs[0] = { + script: Buffer.from(nativeSegwit.scriptPubKey, "hex"), + value: stakingAmountSat, + }; + + expect(() => + stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: modifiedPreviousTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ), + ).toThrow("Previous staking output script type is not P2TR"); + }); + + it(`${networkName} - should throw an error if staking expansion transaction doesn't have exactly 2 inputs`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Create a modified expansion transaction with only 1 input + const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); + modifiedExpansionTx.ins = [modifiedExpansionTx.ins[0]]; // Remove second input + + expect(() => + stakingExpansionPsbt( + network, + modifiedExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ), + ).toThrow("Staking expansion transaction must have exactly 2 inputs"); + }); + + it(`${networkName} - should throw an error if previous staking input hash doesn't match`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Create a modified expansion transaction with wrong hash + const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); + modifiedExpansionTx.ins[0].hash = Buffer.from("0".repeat(64), "hex"); + + expect(() => + stakingExpansionPsbt( + network, + modifiedExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ), + ).toThrow("Previous staking input hash does not match"); + }); + + it(`${networkName} - should throw an error if previous staking input index doesn't match`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Create a modified expansion transaction with wrong index + const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); + modifiedExpansionTx.ins[0].index = 999; + + expect(() => + stakingExpansionPsbt( + network, + modifiedExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ), + ).toThrow("Previous staking input index does not match"); + }); + + it(`${networkName} - should throw an error if input UTXO is not found`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Use different UTXOs that don't match the transaction inputs + const differentUtxos = dataGenerator.generateRandomUTXOs( + 10000, + dataGenerator.getRandomIntegerBetween(1, 10), + ); + + expect(() => + stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + differentUtxos, + previousStakingScript, + ), + ).toThrow(/Input UTXO not found for txid:/); + }); + + }); + + describe("Happy path", () => { + it(`${networkName} - should return a valid PSBT with correct inputs and outputs`, () => { + const { + transaction: stakingExpansionTx, + fundingUTXO, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + const psbt = stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), + ); + + expect(psbt).toBeDefined(); + + // Check PSBT properties + expect(psbt.version).toBe(2); + expect(psbt.locktime).toBe(0); + + // Check inputs + expect(psbt.txInputs.length).toBe(2); + + // First input (previous staking output) + expect(psbt.txInputs[0].hash).toEqual(previousStakingTx.getHash()); + expect(psbt.txInputs[0].index).toBe(0); + expect(psbt.txInputs[0].sequence).toBe(NON_RBF_SEQUENCE); + expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); + expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); + expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(stakingAmountSat); + expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( + previousStakingTx.outs[0].script, + ); + + // Second input (funding UTXO) + expect(psbt.txInputs[1].hash).toEqual(transactionIdToHash(fundingUTXO.txid)); + expect(psbt.txInputs[1].index).toBe(fundingUTXO.vout); + expect(psbt.txInputs[1].sequence).toBe(NON_RBF_SEQUENCE); + + // Check outputs + expect(psbt.txOutputs.length).toBe(stakingExpansionTx.outs.length); + stakingExpansionTx.outs.forEach((output, index) => { + expect(psbt.txOutputs[index].value).toEqual(output.value); + expect(psbt.txOutputs[index].script).toEqual(output.script); + }); + }); + + it(`${networkName} - should work without publicKeyNoCoord parameter`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + const psbt = stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ); + + expect(psbt).toBeDefined(); + expect(psbt.txInputs.length).toBe(2); + + // First input should still have internalPubkey + expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); + + // Second input should not have tapInternalKey when publicKeyNoCoord is not provided + expect(psbt.data.inputs[1].tapInternalKey).toBeUndefined(); + }); + + it(`${networkName} - should have tapInternalKey for second input when publicKeyNoCoord is provided`, () => { + const { taproot } = dataGenerator.getAddressAndScriptPubKey(stakerKeyPair.publicKey); + + // Generate UTXOs with P2TR scriptPubKey for Taproot + const taprootUtxos = dataGenerator.generateRandomUTXOs( + 10000000, // Big enough to cover the fees + dataGenerator.getRandomIntegerBetween(1, 10), + taproot.scriptPubKey, // Use P2TR scriptPubKey + ); + + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + taproot.address, + feeRate, + taprootUtxos, // Use the P2TR UTXOs + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + const psbt = stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + taprootUtxos, // Use the P2TR UTXOs + previousStakingScript, + Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), + ); + + expect(psbt).toBeDefined(); + expect(psbt.txInputs.length).toBe(2); + + // First input should have internalPubkey + expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); + + // Second input should have tapInternalKey when publicKeyNoCoord is provided + expect(psbt.data.inputs[1].tapInternalKey).toEqual( + Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), + ); + }); + + it(`${networkName} - should preserve transaction version and locktime`, () => { + const { + transaction: stakingExpansionTx, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + feeRate, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ); + + // Modify version and locktime + stakingExpansionTx.version = 1; + stakingExpansionTx.locktime = 1000; + + const psbt = stakingExpansionPsbt( + network, + stakingExpansionTx, + { + stakingTx: previousStakingTx, + outputIndex: 0, + }, + utxos, + previousStakingScript, + ); + + expect(psbt.version).toBe(1); + expect(psbt.locktime).toBe(1000); + }); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts new file mode 100644 index 0000000000..cf751a0ee4 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts @@ -0,0 +1,183 @@ +import { address, Psbt } from "bitcoinjs-lib"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { StakingScripts, stakingTransaction } from "../../../src/index"; +import { ObservableStakingScripts } from "../../../src/staking/observable"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; +import { stakingPsbt } from "../../../src/staking/psbt"; + +describe.each(testingNetworks)("Transactions - ", ( + {network, networkName, datagen} +) => { + describe.each(Object.values(datagen))("stakingPsbt", ( + dataGenerator + ) => { + const mockScripts = dataGenerator.generateMockStakingScripts(); + const feeRate = DEFAULT_TEST_FEE_RATE; + const randomAmount = Math.floor(Math.random() * 100000000) + 1000; + // Create enough utxos to cover the amount + const utxos = dataGenerator.generateRandomUTXOs( + randomAmount + 1000000, // let's give enough satoshis to cover the fee + Math.floor(Math.random() * 10) + 1, + ); + describe("Error path", () => { + const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ).taproot.address; + + it(`${networkName} - should throw an error if the public key is invalid`, () => { + const tx = stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + feeRate, + ); + const invalidPublicKey = Buffer.from("invalidPublicKey", "hex"); + expect(() => + stakingPsbt(tx.transaction, network, utxos, invalidPublicKey), + ).toThrow("Invalid public key"); + }); + + it(`${networkName} - should throw an error if the input utxos are not found`, () => { + const tx = stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + feeRate, + ); + const anotherUTXO = dataGenerator.generateRandomUTXOs( + randomAmount + 1000000, // let's give enough satoshis to cover the fee + Math.floor(Math.random() * 10) + 1, + ); + + expect(() => + stakingPsbt(tx.transaction, network, anotherUTXO), + ).toThrow(/Input UTXO not found for txid:/); + }); + }); + + describe("Happy path", () => { + const { taproot, nativeSegwit } = + dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + + it(`${networkName} - should return a valid psbt result with tapInternalKey`, () => { + let txResult = stakingTransaction( + mockScripts, + randomAmount, + taproot.address, + utxos, + network, + feeRate, + ); + + let psbtResult = stakingPsbt( + txResult.transaction, + network, + utxos, + Buffer.from( + dataGenerator.generateRandomKeyPair().publicKeyNoCoord, + "hex", + ), + ); + + validateCommonFields( + psbtResult, + randomAmount, + txResult.fee, + taproot.address, + mockScripts, + ); + + txResult = stakingTransaction( + mockScripts, + randomAmount, + nativeSegwit.address, + utxos, + network, + feeRate, + ); + + psbtResult = stakingPsbt( + txResult.transaction, + network, + utxos, + Buffer.from( + dataGenerator.generateRandomKeyPair().publicKeyNoCoord, + "hex", + ), + ); + + validateCommonFields( + psbtResult, + randomAmount, + txResult.fee, + nativeSegwit.address, + mockScripts, + ); + }); + }); + }); + + const validateCommonFields = ( + psbt: Psbt, + randomAmount: number, + estimatedFee: number, + changeAddress: string, + mockScripts: StakingScripts | ObservableStakingScripts, + ) => { + expect(psbt).toBeDefined(); + // make sure the input amount is greater than the output amount + const inputAmount = psbt.data.inputs.reduce( + (sum, input) => sum + input.witnessUtxo!.value || 0, + 0, + ); + + const outputAmount = psbt.txOutputs.reduce( + (sum, output) => sum + output.value, + 0, + ); + expect(inputAmount).toBeGreaterThan(outputAmount); + expect(inputAmount - outputAmount - estimatedFee).toBeLessThan(BTC_DUST_SAT); + // check the change amount is correct and send to the correct address + if (inputAmount - (randomAmount + estimatedFee) > BTC_DUST_SAT) { + const expectedChangeAmount = inputAmount - (randomAmount + estimatedFee); + const changeOutput = psbt.txOutputs.find( + (output) => output.value === expectedChangeAmount, + ); + expect(changeOutput).toBeDefined(); + // also make sure the change address is correct by look up the `address` + expect( + psbt.txOutputs.find( + (output) => output.script.toString('hex') === address.toOutputScript( + changeAddress, + network, + ).toString('hex'), + ), + ).toBeDefined(); + } + + // check data embed output added to the transaction if the dataEmbedScript is provided + if ((mockScripts as any).dataEmbedScript) { + expect( + psbt.txOutputs.find((output) => + output.script.equals((mockScripts as any).dataEmbedScript), + ), + ).toBeDefined(); + } + // Check the staking amount is correct + expect( + psbt.txOutputs.find((output) => output.value === randomAmount), + ).toBeDefined(); + + psbt.txInputs.map((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + expect(psbt.version).toBe(2); + }; +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts new file mode 100644 index 0000000000..f4191a46b2 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts @@ -0,0 +1,119 @@ +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { stakingTransaction, unbondingTransaction } from "../../../src/index"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; +import { unbondingPsbt } from "../../../src/staking/psbt"; +import { internalPubkey } from "../../../src/constants/internalPubkey"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { Transaction } from "bitcoinjs-lib"; + +describe.each(testingNetworks)("Transactions - ", ( + {network, networkName, datagen} +) => { + describe.each(Object.values(datagen))("unbondingPsbt", ( + dataGenerator + ) => { + const mockScripts = dataGenerator.generateMockStakingScripts(); + const feeRate = DEFAULT_TEST_FEE_RATE; + const params = dataGenerator.generateStakingParams(); + const randomAmount = Math.floor( + Math.random() * 100000000 + ) + 1000 + params.unbondingFeeSat + BTC_DUST_SAT; + // Create enough utxos to cover the amount + const utxos = dataGenerator.generateRandomUTXOs( + randomAmount + 1000000, // let's give enough satoshis to cover the fee + Math.floor(Math.random() * 10) + 1, + ); + const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ).taproot.address; + + const tx = stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + feeRate, + ); + const unbondingTx = unbondingTransaction( + mockScripts, + tx.transaction, + params.unbondingFeeSat, + network, + ); + + it(`${networkName} - should return a valid psbt result with correct inputs and outputs`, () => { + const psbt = unbondingPsbt( + mockScripts, + unbondingTx.transaction, + tx.transaction, + network, + ); + + expect(psbt).toBeDefined(); + + // Check the psbt inputs + expect(psbt.txInputs.length).toBe(1); + expect(psbt.txInputs[0].hash).toEqual(tx.transaction.getHash()); + expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); + expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); + expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(randomAmount); + expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( + tx.transaction.outs[0].script, + ); + expect(psbt.txInputs[0].sequence).toEqual(NON_RBF_SEQUENCE); + expect(psbt.txInputs[0].index).toEqual(0); + // Check the psbt outputs + expect(psbt.txOutputs.length).toBe(1); + expect(psbt.txOutputs[0].value).toEqual(randomAmount - params.unbondingFeeSat); + + // Check the psbt properties + expect(psbt.locktime).toBe(0); + expect(psbt.version).toBe(2); + }); + + it(`${networkName} - should throw error if unbonding tx has more than one output`, () => { + // Create a copy of unbonding tx and add another output + const invalidUnbondingTx = Transaction.fromBuffer(unbondingTx.transaction.toBuffer()); + invalidUnbondingTx.addOutput( + tx.transaction.outs[0].script, + 1000 + ); + + expect(() => unbondingPsbt( + mockScripts, + invalidUnbondingTx, + tx.transaction, + network, + )).toThrow("Unbonding transaction must have exactly one output"); + }); + + it(`${networkName} - should throw error if unbonding tx has more than one input`, () => { + // Create a copy of unbonding tx and add another input + const invalidUnbondingTx = Transaction.fromBuffer(unbondingTx.transaction.toBuffer()); + invalidUnbondingTx.addInput( + tx.transaction.getHash(), + 1, + NON_RBF_SEQUENCE + ); + + expect(() => unbondingPsbt( + mockScripts, + invalidUnbondingTx, + tx.transaction, + network, + )).toThrow("Unbonding transaction must have exactly one input"); + }); + + it(`${networkName} - should throw error if unbonding output script does + not match the expected script`, () => { + const differentScripts = dataGenerator.generateMockStakingScripts(); + expect(() => unbondingPsbt( + differentScripts, + unbondingTx.transaction, + tx.transaction, + network, + )).toThrow("Unbonding output script does not match the expected script while building psbt"); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts new file mode 100644 index 0000000000..7ee348fcd7 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts @@ -0,0 +1,415 @@ +import { opcodes, script } from "bitcoinjs-lib"; +import { StakingScriptData } from "../../src"; + +describe("stakingScript", () => { + const pk1 = Buffer.from( + "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd068", + "hex", + ); + const pk2 = Buffer.from( + "f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8e", + "hex", + ); + const pk3 = Buffer.from( + "17921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4", + "hex", + ); + const pk4 = Buffer.from( + "76d1ae01f8fb6bf30108731c884cddcf57ef6eef2d9d9559e130894e0e40c62c", + "hex", + ); + const pk5 = Buffer.from( + "49766ccd9e3cd94343e2040474a77fb37cdfd30530d05f9f1e96ae1e2102c86e", + "hex", + ); + const invalidPk = Buffer.from( + "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd0", + "hex", + ); + const stakingTimeLock = 65535; + const unbondingTimeLock = 1000; + + describe("Error path", () => { + it("should fail if the staker key is not 32 bytes", () => { + expect( + () => + new StakingScriptData( + invalidPk, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if a finality provider key is not 32 bytes", () => { + expect(() => + new StakingScriptData( + pk1, // Staker Pk + [pk2, invalidPk], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ) + ).toThrow("Invalid script data provided"); + }); + + it("should fail if a covenant emulator key is not 32 bytes", () => { + expect(() => + new StakingScriptData( + pk1, // Staker Pk + [pk2, pk3], // Finality Provider Pks + [pk4, invalidPk, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ) + ).toThrow("Invalid script data provided"); + }); + + it("should fail if the covenant emulators threshold is 0", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 0, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Missing required input values"); + }); + + it("should fail if the covenant emulators threshold is larger than the covenant emulators", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 4, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if the staking timelock is 0", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + 0, + unbondingTimeLock, + ), + ).toThrow("Missing required input values"); + }); + + it("should fail if the staking timelock is above the maximum", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + 65536, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if the unbonding timelock is 0", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + 0, + ), + ).toThrow("Missing required input values"); + }); + + it("should fail if the unbonding timelock is above the maximum", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + 65536, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if the staker pk is in the finality providers list", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2, pk1], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if the staker pk is in the covenants list", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk1, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if a finality provider pk is in the covenants list", () => { + expect( + () => + new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk2, pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + + it("should fail if finality provider have duplicate keys", () => { + expect( + () => + new StakingScriptData( + pk1, + [pk2, pk2], + [pk3, pk4, pk5], + 2, + stakingTimeLock, + unbondingTimeLock, + ), + ).toThrow("Invalid script data provided"); + }); + }); + + describe("Happy path", () => { + it("should succeed with valid input data", () => { + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + expect(scriptData).toBeInstanceOf(StakingScriptData); + }); + + it("should build valid staking timelock script", () => { + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + const timelockScript = scriptData.buildStakingTimelockScript(); + const decompiled = script.decompile(timelockScript); + expect(decompiled).toEqual([ + pk1, + opcodes.OP_CHECKSIGVERIFY, + script.number.encode(stakingTimeLock), + opcodes.OP_CHECKSEQUENCEVERIFY, + ]); + }); + + it("should build valid unbonding timelock script", () => { + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + const unbondingTimelockScript = + scriptData.buildUnbondingTimelockScript(); + const decompiled = script.decompile(unbondingTimelockScript); + expect(decompiled).toEqual([ + pk1, + opcodes.OP_CHECKSIGVERIFY, + script.number.encode(unbondingTimeLock), + opcodes.OP_CHECKSEQUENCEVERIFY, + ]); + }); + + it("should build valid unbonding script", () => { + const pks = [pk3, pk4, pk5]; + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + pks, // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + + const sortedPks = [...pks].sort(Buffer.compare); + + const unbondingScript = scriptData.buildUnbondingScript(); + const decompiled = script.decompile(unbondingScript); + + const expectedScript = script.decompile( + Buffer.concat([ + script.compile([pk1, opcodes.OP_CHECKSIGVERIFY]), + script.compile([ + sortedPks[0], + opcodes.OP_CHECKSIG, + sortedPks[1], + opcodes.OP_CHECKSIGADD, + sortedPks[2], + opcodes.OP_CHECKSIGADD, + script.number.encode(2), + opcodes.OP_NUMEQUAL, + ]), + ]), + ); + + expect(decompiled).toEqual(expectedScript); + }); + + it("should build valid slashing script", () => { + const pks = [pk3, pk4, pk5]; + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + pks, // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + + const sortedPks = [...pks].sort(Buffer.compare); + + const slashingScript = scriptData.buildSlashingScript(); + const decompiled = script.decompile(slashingScript); + + const expectedScript = script.decompile( + Buffer.concat([ + script.compile([pk1, opcodes.OP_CHECKSIGVERIFY]), + script.compile([pk2, opcodes.OP_CHECKSIGVERIFY]), + script.compile([ + sortedPks[0], + opcodes.OP_CHECKSIG, + sortedPks[1], + opcodes.OP_CHECKSIGADD, + sortedPks[2], + opcodes.OP_CHECKSIGADD, + script.number.encode(2), + opcodes.OP_NUMEQUAL, + ]), + ]), + ); + + expect(decompiled).toEqual(expectedScript); + }); + + it("should build valid staking scripts", () => { + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + const scripts = scriptData.buildScripts(); + expect(scripts).toHaveProperty("timelockScript"); + expect(scripts).toHaveProperty("unbondingScript"); + expect(scripts).toHaveProperty("slashingScript"); + expect(scripts).toHaveProperty("unbondingTimelockScript"); + // We don't expect the data embed script to be present + expect(scripts).not.toHaveProperty("dataEmbedScript"); + }); + + it("should validate correctly with valid input data", () => { + const scriptData = new StakingScriptData( + pk1, // Staker Pk + [pk2], // Finality Provider Pks + [pk3, pk4, pk5], // covenant Pks + 2, + stakingTimeLock, + unbondingTimeLock, + ); + expect(scriptData.validate()).toBe(true); + }); + + it("should validate correctly with minimum valid staking and unbonding timelock", () => { + const scriptData = new StakingScriptData( + pk1, + [pk2], + [pk3, pk4, pk5], + 2, + 1, // Minimum valid staking timelock + 1, // Minimum valid unbonding timelock + ); + expect(scriptData.validate()).toBe(true); + }); + + it("should validate correctly with unique keys", () => { + const scriptData = new StakingScriptData( + pk1, + [pk2], + [pk3, pk4, pk5], + 2, + stakingTimeLock, + unbondingTimeLock, + ); + expect(scriptData.validate()).toBe(true); + }); + + it("should handle maximum valid staking and unbonding timelock", () => { + const scriptData = new StakingScriptData( + pk1, + [pk2], + [pk3, pk4, pk5], + 2, + 65535, // Maximum valid staking timelock + 65535, // Maximum valid unbonding timelock + ); + expect(scriptData.validate()).toBe(true); + }); + + it("should not fail for more than 1 finality provider", () => { + const scriptData = new StakingScriptData( + pk1, + [pk2, pk3], + [pk4, pk5], + 2, + stakingTimeLock, + unbondingTimeLock, + ); + expect(scriptData.validate()).toBe(true); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts new file mode 100644 index 0000000000..d6f31e875f --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts @@ -0,0 +1,360 @@ +import { payments } from "bitcoinjs-lib"; +import { + slashEarlyUnbondedTransaction, + slashTimelockUnbondedTransaction, + unbondingTransaction, +} from "../../../src"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { internalPubkey } from "../../../src/constants/internalPubkey"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; +import { NON_RBF_SEQUENCE, TRANSACTION_VERSION } from "../../../src/constants/psbt"; +import { getRandomPaymentScriptHex } from "../../helper/datagen/base"; + +describe.each(testingNetworks)("Transactions - ", ( + {network, networkName, datagen} +) => { + describe.each(Object.values(datagen))("slashingTransaction - ", ( + dataGenerator + ) => { + const stakerKeyPair = dataGenerator.generateRandomKeyPair(); + const stakingScripts = + dataGenerator.generateMockStakingScripts(stakerKeyPair); + const { stakingTx, stakingAmountSat} = dataGenerator.generateRandomStakingTransaction( + network, + DEFAULT_TEST_FEE_RATE, + stakerKeyPair, + ); + const slashingRate = dataGenerator.generateRandomSlashingRate(); + const slashingAmount = Math.round(stakingAmountSat * slashingRate); + const minSlashingFee = dataGenerator.getRandomIntegerBetween( + 1, + stakingAmountSat - slashingAmount - BTC_DUST_SAT - 1, + ); + const defaultOutputIndex = 0; + const slashingPkScriptHex = getRandomPaymentScriptHex( + dataGenerator.generateRandomKeyPair().publicKey, + ); + + describe(`${networkName} - slashTimelockUnbondedTransaction`, () => { + it("should throw an error if the slashing rate is not between 0 and 1", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + 0, + minSlashingFee, + network, + defaultOutputIndex, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + -0.1, + minSlashingFee, + network, + defaultOutputIndex, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + 1, + minSlashingFee, + network, + defaultOutputIndex, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + 1.1, + minSlashingFee, + network, + defaultOutputIndex, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + }); + + it("should throw an error if minimum slashing fee is less than 0", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + 0, + network, + defaultOutputIndex, + ), + ).toThrow("Minimum fee must be a positve integer"); + }); + + it("should throw an error if minimum slashing fee is not integer", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + 1.2, + network, + defaultOutputIndex, + ), + ).toThrow("Minimum fee must be a positve integer"); + }); + + it("should throw an error if the output index is less than 0", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + -1, + ), + ).toThrow("Output index must be an integer bigger or equal to 0"); + }); + + it("should throw an error if the output index is not integer", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + 1.2, + ), + ).toThrow("Output index must be an integer bigger or equal to 0"); + + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + 0.5, + ), + ).toThrow("Output index must be an integer bigger or equal to 0"); + }); + + it("should throw an error if the output index is greater than the number of outputs", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + stakingTx.outs.length, + ), + ).toThrow("Output index is out of range"); + }); + + it("should throw error if user funds after slashing and fees is less than dust", () => { + expect(() => + slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + Math.ceil(stakingAmountSat * (1 - slashingRate) + 1), + network, + 0, + ), + ).toThrow("User funds are less than dust limit"); + }); + + it("should create the slashing time lock unbonded tx psbt successfully", () => { + const { psbt } = slashTimelockUnbondedTransaction( + stakingScripts, + stakingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + 0, + ); + + expect(psbt).toBeDefined(); + expect(psbt.txOutputs.length).toBe(2); + // first output shall send slashed amount to the slashing script + expect(Buffer.from(psbt.txOutputs[0].script).toString("hex")).toBe(slashingPkScriptHex); + expect(psbt.txOutputs[0].value).toBe( + Math.round(stakingAmountSat * slashingRate), + ); + + // second output is the change output which send to unbonding timelock script address + const changeOutput = payments.p2tr({ + internalPubkey, + scriptTree: { output: stakingScripts.unbondingTimelockScript }, + network, + }); + expect(psbt.txOutputs[1].address).toBe(changeOutput.address); + const expectedChangeOutputValue = + stakingAmountSat - + Math.round(stakingAmountSat * slashingRate) - + minSlashingFee; + expect(psbt.txOutputs[1].value).toBe(expectedChangeOutputValue); + }); + }); + + describe(`${networkName} slashEarlyUnbondedTransaction - `, () => { + const { transaction: unbondingTx } = unbondingTransaction( + stakingScripts, + stakingTx, + 1, + network, + ); + + it("should throw an error if the slashing rate is not between 0 and 1", () => { + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + 0, + minSlashingFee, + network, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + -0.1, + minSlashingFee, + network, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + 1, + minSlashingFee, + network, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + 1.1, + minSlashingFee, + network, + ), + ).toThrow("Slashing rate must be between 0 and 1"); + }); + + it("should throw an error if minimum slashing fee is less than 0", () => { + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + slashingRate, + 0, + network, + ), + ).toThrow("Minimum fee must be a positve integer"); + }); + + it("should throw error if user funds is less than dust", () => { + const { transaction: unbondingTxWithLimitedAmount } = unbondingTransaction( + stakingScripts, + stakingTx, + 1, + network, + ); + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTxWithLimitedAmount, + slashingPkScriptHex, + slashingRate, + Math.ceil(stakingAmountSat * (1 - slashingRate) + 1), + network, + ), + ).toThrow("User funds are less than dust limit"); + }); + + it("should throw if its slashing amount is less than dust", () => { + const smallSlashingRate = BTC_DUST_SAT / stakingAmountSat; + expect(() => + slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + smallSlashingRate, + minSlashingFee, + network, + ) + ).toThrow("Slashing amount is less than dust limit"); + }); + + it("should create the slashing time lock unbonded tx psbt successfully", () => { + const { psbt } = slashEarlyUnbondedTransaction( + stakingScripts, + unbondingTx, + slashingPkScriptHex, + slashingRate, + minSlashingFee, + network, + ); + + const unbondingTxOutputValue = unbondingTx.outs[0].value; + + expect(psbt).toBeDefined(); + expect(psbt.txOutputs.length).toBe(2); + // first output shall send slashed amount to the slashing pk script (i.e burn output) + expect(Buffer.from(psbt.txOutputs[0].script).toString("hex")).toBe(slashingPkScriptHex); + expect(psbt.txOutputs[0].value).toBe( + Math.round(unbondingTxOutputValue * slashingRate), + ); + + // second output is the change output which send to unbonding timelock script address + const changeOutput = payments.p2tr({ + internalPubkey, + scriptTree: { output: stakingScripts.unbondingTimelockScript }, + network, + }); + expect(psbt.txOutputs[1].address).toBe(changeOutput.address); + const expectedChangeOutputValue = + unbondingTxOutputValue - + Math.round(unbondingTxOutputValue * slashingRate) - + minSlashingFee; + expect(psbt.txOutputs[1].value).toBe(expectedChangeOutputValue); + + expect(psbt.version).toBe(TRANSACTION_VERSION); + expect(psbt.locktime).toBe(0); + psbt.txInputs.forEach((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + }); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts new file mode 100644 index 0000000000..128d87010c --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts @@ -0,0 +1,200 @@ +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { stakingExpansionTransaction } from "../../../src/staking/transactions"; +import { transactionIdToHash } from "../../../src/utils/btc"; +import { testingNetworks } from "../../helper"; + +describe("stakingExpansionTransaction", () => { + const [mainnet] = testingNetworks; + const { datagen: { stakingDatagen }, network } = mainnet; + const stakerKeyPair = stakingDatagen.generateRandomKeyPair(); + const { + stakingTx: previousStakingTx, + stakingAmountSat, + stakerInfo, + scriptPubKey, + stakingInstance: previousStakingInstance, + } = stakingDatagen.generateRandomStakingTransaction( + network, + 1, + stakerKeyPair, + ); + + const previousStakingScript = previousStakingInstance.buildScripts(); + const utxos = stakingDatagen.generateRandomUTXOs( + 10000, // Big enough to cover the fees + stakingDatagen.getRandomIntegerBetween(1, 10), + scriptPubKey, + ); + + it("should successfully expand a staking transaction", () => { + const { + transaction: stakingExpansionTx, + fee: stakingExpansionTxFee, + fundingUTXO, + } = stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + 1, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + expect(stakingExpansionTx).toBeDefined(); + expect(stakingExpansionTxFee).toBeGreaterThan(0); + // Must have two inputs: + // 1. The previous staking transaction output + // 2. The funding UTXO + expect(stakingExpansionTx.ins.length).toBe(2); + // First output must be the previous staking transaction output + expect(stakingExpansionTx.ins[0].hash).toEqual(previousStakingTx.getHash()); + // First output amount must match the previous staking amount + + // Must have more than or equal to 1 output + expect(stakingExpansionTx.outs.length).toBeGreaterThanOrEqual(1); + // Must match the same staking amount as previous staking transaction + expect(stakingExpansionTx.outs[0].value).toBe(stakingAmountSat); + + // Find the matching UTXO from the inputUTXOs list so that we know the amount. + const fundingUtxo = utxos.find( + (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) + ); + expect(fundingUtxo).toBeDefined(); + if (fundingUtxo!.value - stakingExpansionTxFee > BTC_DUST_SAT) { + // Must have a change output as the last output + expect(stakingExpansionTx.outs[ + stakingExpansionTx.outs.length - 1 + ].value).toBe(fundingUtxo!.value - stakingExpansionTxFee); + } + + // Both inputs should have the same sequence number (non-RBF) + expect(stakingExpansionTx.ins[0].sequence).toBe(NON_RBF_SEQUENCE); + expect(stakingExpansionTx.ins[1].sequence).toBe(NON_RBF_SEQUENCE); + // Should use standard transaction version + expect(stakingExpansionTx.version).toBe(2); + + expect(fundingUTXO).toBeDefined(); + expect(fundingUTXO.value).toBeGreaterThan(0); + + // Funding UTXO should be the same as the selected UTXO + expect(fundingUTXO.txid).toEqual(utxos.find( + (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) + )!.txid); + expect(fundingUTXO.value).toEqual(utxos.find( + (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) + )!.value); + }); + + it("should throw error when amount is less than or equal to 0", () => { + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + 0, // Invalid amount + stakerInfo.address, + 1, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Amount and fee rate must be bigger than 0"); + }); + + it("should throw error when fee rate is less than or equal to 0", () => { + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + 0, // Invalid fee rate + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Amount and fee rate must be bigger than 0"); + }); + + it("should throw error when change address is invalid", () => { + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + "invalid_address", // Invalid address + 1, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Invalid BTC change address"); + }); + + it("should throw error when expansion amount does not match previous staking amount", () => { + const differentAmount = stakingAmountSat + 1000; // Different amount + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + differentAmount, + stakerInfo.address, + 1, + utxos, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Expansion staking transaction amount must be equal to the previous staking amount"); + }); + + it("should throw error when no UTXOs are available for funding", () => { + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + 1, + [], // Empty UTXOs + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Insufficient funds"); + }); + + it("should throw error when no UTXOs can cover the required fees", () => { + // Create a UTXO with a value less than the fees + const smallUtxo = stakingDatagen.generateRandomUTXOs( + 100, // Much less than the fees + 1, + scriptPubKey, + ); + expect(() => + stakingExpansionTransaction( + network, + previousStakingScript, + stakingAmountSat, + stakerInfo.address, + 1, + smallUtxo, + { + stakingTx: previousStakingTx, + scripts: previousStakingScript, + }, + ) + ).toThrow("Insufficient funds: unable to find a UTXO to cover the fees"); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts new file mode 100644 index 0000000000..04989d88b4 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts @@ -0,0 +1,381 @@ +import { address } from "bitcoinjs-lib"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; +import { StakingScripts, stakingTransaction, transactionIdToHash, UTXO } from "../../../src/index"; +import { ObservableStakingScripts } from "../../../src/staking/observable"; +import { TransactionResult } from "../../../src/types/transaction"; +import { getStakingTxInputUTXOsAndFees } from "../../../src/utils/fee"; +import { buildStakingTransactionOutputs } from "../../../src/utils/staking"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; + +describe("StakingTransaction - Cross env error", () => { + const [mainnet, testnet] = testingNetworks; + const envPairs = [ + { + mainnetDataGenerator: mainnet.datagen.stakingDatagen, + testnetDataGenerator: testnet.datagen.stakingDatagen, + }, + ]; + + envPairs.map(({ mainnetDataGenerator, testnetDataGenerator }) => { + const randomAmount = Math.floor(Math.random() * 100000000) + 1000; + + it("should throw an error if the testnet inputs are used on mainnet", () => { + const randomChangeAddress = + testnetDataGenerator.getAddressAndScriptPubKey( + mainnetDataGenerator.generateRandomKeyPair().publicKey, + ).nativeSegwit.address; + const utxos = testnetDataGenerator.generateRandomUTXOs( + randomAmount + 1000000, + Math.floor(Math.random() * 10) + 1, + ); + expect(() => + stakingTransaction( + testnetDataGenerator.generateMockStakingScripts(), + randomAmount, + randomChangeAddress, + utxos, + mainnet.network, + 1, + ), + ).toThrow("Invalid change address"); + }); + + it("should throw an error if the mainnet inputs are used on testnet", () => { + const randomChangeAddress = + mainnetDataGenerator.getAddressAndScriptPubKey( + mainnetDataGenerator.generateRandomKeyPair().publicKey, + ).nativeSegwit.address; + const utxos = mainnetDataGenerator.generateRandomUTXOs( + randomAmount + 1000000, + Math.floor(Math.random() * 10) + 1, + ); + expect(() => + stakingTransaction( + mainnetDataGenerator.generateMockStakingScripts(), + randomAmount, + randomChangeAddress, + utxos, + testnet.network, + 1, + ), + ).toThrow("Invalid change address"); + }); + }); +}); + +describe.each(testingNetworks)("Transactions - ", ( + {network, networkName, datagen} +) => { + describe.each(Object.values(datagen))("stakingTransaction", ( + dataGenerator + ) => { + const mockScripts = dataGenerator.generateMockStakingScripts(); + const feeRate = DEFAULT_TEST_FEE_RATE; + const randomAmount = Math.floor(Math.random() * 100000000) + 1000; + // Create enough utxos to cover the amount + const utxos = dataGenerator.generateRandomUTXOs( + randomAmount + 1000000, // let's give enough satoshis to cover the fee + Math.floor(Math.random() * 10) + 1, + ); + describe("Error path", () => { + const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ).taproot.address; + + it(`${networkName} - should throw an error if the change address is invalid`, () => { + const validAddress = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ).taproot.address; + const invalidCharInAddress = validAddress.replace(validAddress[0], "I"); // I is an invalid character in base58 + const invalidAddressLegnth = validAddress.slice(0, -1); + const invalidAddresses = [ + "", + " ", + "banana", + invalidCharInAddress, + invalidAddressLegnth, + ]; + invalidAddresses.map((a) => { + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + a, // Invalid address + utxos, + network, + feeRate, + ), + ).toThrow("Invalid change address"); + }); + }); + + it(`${networkName} - should throw an error if the utxo value is too low`, () => { + // generate a UTXO that is too small to cover the fee + const scriptPubKey = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ).taproot.scriptPubKey; + const utxo = { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: scriptPubKey, + value: 1, + }; + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + [utxo], + network, + 1, + ), + ).toThrow( + "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees", + ); + }); + + it(`${networkName} - should ignore the invalid utxo if the utxo scriptPubKey is invalid`, () => { + const utxo = { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: `abc${dataGenerator.generateRandomKeyPair().publicKey}`, // this is not a valid scriptPubKey + value: 10000000000000, + }; + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + [utxo], + network, + 1, + ), + ).toThrow("Insufficient funds: no valid UTXOs available for staking") + }); + + it(`${networkName} - should throw an error if UTXO is empty`, () => { + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + [], + network, + 1, + ), + ).toThrow("Insufficient funds"); + }); + + it(`${networkName} - should throw an error if the lock height is invalid`, () => { + // 500000000 is the maximum lock height in btc + const invalidLockHeight = 500000000 + 1; + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + feeRate, + invalidLockHeight, + ), + ).toThrow("Invalid lock height"); + }); + + it(`${networkName} - should throw an error if the amount is less than or equal to 0`, () => { + // Test case: amount is 0 + expect(() => + stakingTransaction( + mockScripts, + 0, // Invalid amount + randomChangeAddress, + utxos, + network, + dataGenerator.generateRandomFeeRates(), // Valid fee rate + ), + ).toThrow("Amount and fee rate must be bigger than 0"); + + // Test case: amount is -1 + expect(() => + stakingTransaction( + mockScripts, + -1, // Invalid amount + randomChangeAddress, + utxos, + network, + dataGenerator.generateRandomFeeRates(), // Valid fee rate + ), + ).toThrow("Amount and fee rate must be bigger than 0"); + }); + + it("should throw an error if the fee rate is less than or equal to 0", () => { + // Test case: fee rate is 0 + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + 0, // Invalid fee rate + ), + ).toThrow("Amount and fee rate must be bigger than 0"); + + // Test case: fee rate is -1 + expect(() => + stakingTransaction( + mockScripts, + randomAmount, + randomChangeAddress, + utxos, + network, + -1, // Invalid fee rate + ), + ).toThrow("Amount and fee rate must be bigger than 0"); + }); + }); + + describe("Happy path", () => { + // build the outputs + const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); + // A rough estimating of the fee, the end result should not be too far from this + const { fee: estimatedFee } = getStakingTxInputUTXOsAndFees( + utxos, + randomAmount, + feeRate, + outputs, + ); + const { taproot, nativeSegwit } = + dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + + it(`${networkName} - should return a valid transaction result`, () => { + const transactionResultTaproot = stakingTransaction( + mockScripts, + randomAmount, + taproot.address, + utxos, + network, + feeRate, + ); + validateCommonFields( + transactionResultTaproot, + utxos, + randomAmount, + estimatedFee, + taproot.address, + mockScripts, + ); + + const transactionResultNativeSegwit = stakingTransaction( + mockScripts, + randomAmount, + nativeSegwit.address, + utxos, + network, + feeRate, + ); + validateCommonFields( + transactionResultNativeSegwit, + utxos, + randomAmount, + estimatedFee, + nativeSegwit.address, + mockScripts, + ); + }); + + it(`${networkName} - should return a valid transaction result with lock field`, () => { + const lockHeight = Math.floor(Math.random() * 1000000) + 100; + const transactionResult = stakingTransaction( + mockScripts, + randomAmount, + taproot.address, + utxos, + network, + feeRate, + lockHeight, + ); + validateCommonFields( + transactionResult, + utxos, + randomAmount, + estimatedFee, + taproot.address, + mockScripts, + ); + // check the lock height is correct + expect(transactionResult.transaction.locktime).toEqual(lockHeight); + }); + }); + }); + + const validateCommonFields = ( + transactionResult: TransactionResult, + utxos: UTXO[], + randomAmount: number, + estimatedFee: number, + changeAddress: string, + mockScripts: StakingScripts | ObservableStakingScripts, + ) => { + expect(transactionResult).toBeDefined(); + // expect the estimated fee and the actual fee is the same + expect(transactionResult.fee).toBe(estimatedFee); + // make sure the input amount is greater than the output amount + const { transaction, fee } = transactionResult; + const inputAmount = transaction.ins.reduce( + (sum, input) => { + const id = input.hash.toString("hex"); + const utxo = utxos.find((utxo) => + transactionIdToHash(utxo.txid).toString("hex") === id + && utxo.vout === input.index, + ); + return sum + utxo!.value; + }, + 0, + ); + const outputAmount = transaction.outs.reduce( + (sum, output) => sum + output.value, + 0, + ); + expect(inputAmount).toBeGreaterThan(outputAmount); + expect(inputAmount - outputAmount - fee).toBeLessThan(BTC_DUST_SAT); + // check the change amount is correct and send to the correct address + if (inputAmount - (randomAmount + fee) > BTC_DUST_SAT) { + const expectedChangeAmount = inputAmount - (randomAmount + fee); + const changeOutput = transaction.outs.find( + (output) => output.value === expectedChangeAmount, + ); + expect(changeOutput).toBeDefined(); + // also make sure the change address is correct by look up the `address` + expect( + transaction.outs.find( + (output) => output.script.toString('hex') === address.toOutputScript( + changeAddress, + network, + ).toString('hex'), + ), + ).toBeDefined(); + } + + // check data embed output added to the transaction if the dataEmbedScript is provided + if ((mockScripts as any).dataEmbedScript) { + expect( + transaction.outs.find((output) => + output.script.equals((mockScripts as any).dataEmbedScript), + ), + ).toBeDefined(); + } + // Check the staking amount is correct + expect( + transaction.outs.find((output) => output.value === randomAmount), + ).toBeDefined(); + + transaction.ins.map((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + expect(transaction.version).toBe(2); + }; +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts new file mode 100644 index 0000000000..cb13a7fbe6 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts @@ -0,0 +1,86 @@ +import { unbondingTransaction } from "../../../src"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { + NON_RBF_SEQUENCE, + TRANSACTION_VERSION, +} from "../../../src/constants/psbt"; +import { testingNetworks } from "../../helper"; + +describe.each(testingNetworks)( + "Transactions - ", + ({ networkName, network, datagen }) => { + describe.each(Object.values(datagen))( + "unbondingTransaction", + (dataGenerator) => { + const stakerKeyPair = dataGenerator.generateRandomKeyPair(); + const { stakingTx, stakingAmountSat } = + dataGenerator.generateRandomStakingTransaction( + network, + 1, + stakerKeyPair, + ); + const stakingScripts = + dataGenerator.generateMockStakingScripts(stakerKeyPair); + + describe(`${networkName} - `, () => { + it("should throw an error if the unbonding fee is not postive number", () => { + expect(() => + unbondingTransaction(stakingScripts, stakingTx, 0, network), + ).toThrow("Unbonding fee must be bigger than 0"); + }); + + it("should throw if output index is negative", () => { + expect(() => + unbondingTransaction( + stakingScripts, + stakingTx, + dataGenerator.getRandomIntegerBetween(1, 10000), + network, + -1, + ), + ).toThrow("Output index must be bigger or equal to 0"); + }); + + it("should throw if output is less than dust limit", () => { + const unbondingFee = stakingAmountSat - BTC_DUST_SAT + 1; + expect(() => + unbondingTransaction( + stakingScripts, + stakingTx, + unbondingFee, + network, + 0, + ), + ).toThrow("Output value is less than dust limit"); + }); + + it("should return psbt for unbonding transaction", () => { + const unbondingFee = dataGenerator.getRandomIntegerBetween( + 1, + stakingAmountSat - BTC_DUST_SAT - 1, + ); + const { transaction } = unbondingTransaction( + stakingScripts, + stakingTx, + unbondingFee, + network, + 0, + ); + expect(transaction).toBeDefined(); + expect(transaction.outs.length).toBe(1); + // check output value + expect(transaction.outs[0].value).toBe( + stakingAmountSat - unbondingFee, + ); + + expect(transaction.locktime).toBe(0); + expect(transaction.version).toBe(TRANSACTION_VERSION); + transaction.ins.forEach((input) => { + expect(input.sequence).toBe(NON_RBF_SEQUENCE); + }); + }); + }); + }, + ); + }, +); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts new file mode 100644 index 0000000000..e732d7e496 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts @@ -0,0 +1,326 @@ +import { Network, Transaction, script } from "bitcoinjs-lib"; +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { TRANSACTION_VERSION } from "../../../src/constants/psbt"; +import { + StakingParams, + StakingScripts, + withdrawEarlyUnbondedTransaction, + withdrawSlashingTransaction, + withdrawTimelockUnbondedTransaction, +} from "../../../src/index"; +import { StakerInfo } from "../../../src/staking"; +import { PsbtResult } from "../../../src/types/transaction"; +import { getWithdrawTxFee } from "../../../src/utils/fee"; +import { + deriveSlashingOutput, + findMatchingTxOutputIndex, +} from "../../../src/utils/staking"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; +import { + KeyPair, + SlashingType, + StakingDataGenerator, +} from "../../helper/datagen/base"; +import { ObservableStakingDatagen } from "../../helper/datagen/observable"; + +interface WithdrawTransactionTestData { + keyPair: KeyPair; + stakerInfo: StakerInfo; + stakingScripts: StakingScripts; + stakingTx: Transaction; + stakingAmountSat: number; + params: StakingParams; +} + +const setupTestData = ( + network: Network, + dataGenerator: ObservableStakingDatagen | StakingDataGenerator, +): WithdrawTransactionTestData => { + const stakerKeyPair = dataGenerator.generateRandomKeyPair(); + + const stakingScripts = + dataGenerator.generateMockStakingScripts(stakerKeyPair); + const { stakingTx, stakerInfo, params, stakingAmountSat } = + dataGenerator.generateRandomStakingTransaction(network, 1, stakerKeyPair); + + return { + keyPair: stakerKeyPair, + stakerInfo, + stakingScripts, + stakingTx, + stakingAmountSat, + params, + }; +}; + +describe.each(testingNetworks)( + "withdrawTransaction", + ({ networkName, network, datagen }) => { + describe.each(Object.values(datagen))( + "withdrawTransaction", + (dataGenerator) => { + let testData: WithdrawTransactionTestData; + + beforeEach(() => { + jest.restoreAllMocks(); + testData = setupTestData(network, dataGenerator); + }); + + it(`${networkName} - should throw an error if the fee rate is less than or equal to 0`, () => { + expect(() => + withdrawEarlyUnbondedTransaction( + { + unbondingTimelockScript: + testData.stakingScripts.unbondingTimelockScript, + slashingScript: testData.stakingScripts.slashingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + 0, + ), + ).toThrow("Withdrawal feeRate must be bigger than 0"); + + expect(() => + withdrawTimelockUnbondedTransaction( + { + timelockScript: testData.stakingScripts.timelockScript, + slashingScript: testData.stakingScripts.slashingScript, + unbondingScript: testData.stakingScripts.unbondingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + 0, + ), + ).toThrow("Withdrawal feeRate must be bigger than 0"); + + expect(() => + withdrawEarlyUnbondedTransaction( + { + unbondingTimelockScript: + testData.stakingScripts.unbondingTimelockScript, + slashingScript: testData.stakingScripts.slashingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + -1, + ), + ).toThrow("Withdrawal feeRate must be bigger than 0"); + + expect(() => + withdrawTimelockUnbondedTransaction( + { + timelockScript: testData.stakingScripts.timelockScript, + slashingScript: testData.stakingScripts.slashingScript, + unbondingScript: testData.stakingScripts.unbondingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + -1, + ), + ).toThrow("Withdrawal feeRate must be bigger than 0"); + }); + + it(`${networkName} - should throw an error if the timelock script is not valid`, () => { + // mock decompile to return null + jest.spyOn(script, "decompile").mockReturnValue(null); + expect(() => + withdrawTimelockUnbondedTransaction( + { + timelockScript: Buffer.alloc(1), + slashingScript: testData.stakingScripts.slashingScript, + unbondingScript: testData.stakingScripts.unbondingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + ), + ).toThrow("Timelock script is not valid"); + }); + + it(`${networkName} - should throw an error if output index is invalid`, () => { + expect(() => + withdrawTimelockUnbondedTransaction( + { + timelockScript: testData.stakingScripts.timelockScript, + slashingScript: testData.stakingScripts.slashingScript, + unbondingScript: testData.stakingScripts.unbondingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + -1, + ), + ).toThrow("Output index must be bigger or equal to 0"); + }); + + it(`${networkName} - should throw if not enough funds to cover fees`, () => { + const { stakingTx, stakerInfo, keyPair, stakingAmountSat } = + dataGenerator.generateRandomStakingTransaction(network); + const stakingScripts = + dataGenerator.generateMockStakingScripts(keyPair); + const unitWithdrawTxFee = getWithdrawTxFee(1); + const feeRateToExceedAmount = + Math.ceil(stakingAmountSat / unitWithdrawTxFee) * 10; + + expect(() => + withdrawEarlyUnbondedTransaction( + { + unbondingTimelockScript: stakingScripts.unbondingTimelockScript, + slashingScript: stakingScripts.slashingScript, + }, + stakingTx, + stakerInfo.address, + network, + feeRateToExceedAmount, + ), + ).toThrow( + "Not enough funds to cover the fee for withdrawal transaction", + ); + }); + + it(`${networkName} - should throw if output is less than dust limit`, () => { + const params = dataGenerator.generateStakingParams( + false, + undefined, + 0, + ); + const estimatedFee = getWithdrawTxFee(DEFAULT_TEST_FEE_RATE); + const amountToNotCoverDustLimit = + BTC_DUST_SAT + estimatedFee - 1 + params.unbondingFeeSat; + + const { stakingTx, stakerInfo, stakingInstance, stakingTxFee } = + dataGenerator.generateRandomStakingTransaction( + network, + DEFAULT_TEST_FEE_RATE, + undefined, + amountToNotCoverDustLimit, + undefined, + params, + ); + const { transaction: unbondingTx } = + stakingInstance.createUnbondingTransaction(stakingTx); + expect(() => + withdrawEarlyUnbondedTransaction( + stakingInstance.buildScripts(), + unbondingTx, + stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + ), + ).toThrow("Output value is less than dust limit"); + }); + + it(`${networkName} - should return a valid psbt result for early unbonded transaction`, () => { + const psbtResult = withdrawEarlyUnbondedTransaction( + { + unbondingTimelockScript: + testData.stakingScripts.unbondingTimelockScript, + slashingScript: testData.stakingScripts.slashingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + ); + validateCommonFields(psbtResult, testData.stakerInfo.address); + }); + + it(`${networkName} - should return a valid psbt result for timelock unbonded transaction`, () => { + const psbtResult = withdrawTimelockUnbondedTransaction( + { + timelockScript: testData.stakingScripts.timelockScript, + slashingScript: testData.stakingScripts.slashingScript, + unbondingScript: testData.stakingScripts.unbondingScript, + }, + testData.stakingTx, + testData.stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + ); + validateCommonFields(psbtResult, testData.stakerInfo.address); + }); + + it(`${networkName} - should create the withdraw slashing transactions successfully`, () => { + const slashingTypes: SlashingType[] = [ + "earlyUnbonded", + "timelockExpire", + ]; + slashingTypes.forEach((type) => { + const { tx: slashingTx } = + dataGenerator.generateSlashingTransaction( + network, + testData.stakingScripts, + testData.stakingTx, + { + minSlashingTxFeeSat: + testData.params.slashing?.minSlashingTxFeeSat!!, + slashingPkScriptHex: + testData.params.slashing?.slashingPkScriptHex!!, + slashingRate: testData.params.slashing?.slashingRate!!, + }, + testData.keyPair, + type, + ); + + const outputIndex = findMatchingTxOutputIndex( + slashingTx, + deriveSlashingOutput(testData.stakingScripts, network) + .outputAddress, + network, + ); + + const psbt = withdrawSlashingTransaction( + testData.stakingScripts, + slashingTx, + testData.stakerInfo.address, + network, + DEFAULT_TEST_FEE_RATE, + outputIndex, + ); + validateCommonFields(psbt, testData.stakerInfo.address); + + // Validate the slashing output value + const remainingAmout = + slashingTx.outs[outputIndex].value - + getWithdrawTxFee(DEFAULT_TEST_FEE_RATE); + expect(psbt.psbt.txOutputs[0].value).toBe( + Math.floor(remainingAmout), + ); + }); + }); + }, + ); + }, +); + +const validateCommonFields = ( + psbtResult: PsbtResult, + withdrawalAddress: string, +) => { + expect(psbtResult).toBeDefined(); + const { psbt, fee } = psbtResult; + const inputAmount = psbt.data.inputs.reduce( + (sum, input) => sum + input.witnessUtxo!.value, + 0, + ); + const outputAmount = psbt.txOutputs.reduce( + (sum, output) => sum + output.value, + 0, + ); + expect(inputAmount).toBeGreaterThan(outputAmount); + expect(inputAmount - outputAmount).toEqual(fee); + expect( + psbt.txOutputs.find((output) => output.address === withdrawalAddress), + ).toBeDefined(); + + // validate the psbt version + expect(psbt.version).toBe(TRANSACTION_VERSION); + // validate the locktime + expect(psbt.locktime).toBe(0); +}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts new file mode 100644 index 0000000000..ac06ea0127 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts @@ -0,0 +1,367 @@ +import * as utils from '../../src/utils/staking'; +import { testingNetworks } from '../helper'; +import { Staking } from "../../src/staking"; + +describe.each(testingNetworks)("Staking input validations", ({ + network, datagen: { stakingDatagen: dataGenerator } +}) => { + describe('validateDelegationInputs', () => { + const params = dataGenerator.generateStakingParams(true); + const feeRate = 1; + const { + stakingTx, timelock, stakerInfo, finalityProviderPksNoCoordHex, + } = dataGenerator.generateRandomStakingTransaction( + network, feeRate, undefined, undefined, undefined, params, + ); + + const stakingInstance = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + beforeEach(() => { + jest.restoreAllMocks(); + }); + + it('should throw an error if the timelock is out of range', () => { + expect(() => { + new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, params.minStakingTimeBlocks - 1, + ); + }).toThrow('Staking transaction timelock is out of range'); + + expect(() => { + new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, params.maxStakingTimeBlocks + 1, + ); + }).toThrow('Staking transaction timelock is out of range'); + }); + + it('should throw an error if the output index is out of range', () => { + jest.spyOn(utils, "findMatchingTxOutputIndex").mockImplementation(() => { + throw new Error('Staking transaction output index is out of range'); + }); + expect(() => { + stakingInstance.createWithdrawStakingExpiredPsbt( + stakingTx, feeRate, + ); + }).toThrow('Staking transaction output index is out of range'); + + expect(() => { + stakingInstance.createUnbondingTransaction( + stakingTx + ); + }).toThrow('Staking transaction output index is out of range'); + }); + }); + + describe('validateParams', () => { + const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); + const { address } = dataGenerator.getAddressAndScriptPubKey( + publicKey, + ).taproot; + + const stakerInfo = { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + publicKeyWithCoord: publicKey, + }; + const finalityProviderPksNoCoordHex: string[] = []; + for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { + finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); + } + const validParams = dataGenerator.generateStakingParams(); + + it('should pass with valid parameters', () => { + expect(() => new Staking( + network, + stakerInfo, + validParams, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).not.toThrow(); + }); + + it('should pass with valid parameters without slashing', () => { + const paramsWithoutSlashing = { ...validParams, slashing: undefined }; + expect(() => new Staking( + network, + stakerInfo, + paramsWithoutSlashing, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).not.toThrow(); + }); + + it('should throw an error if covenant public keys are empty', () => { + const params = { ...validParams, covenantNoCoordPks: [] }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Could not find any covenant public keys' + ); + }); + + it('should throw an error if covenant public keys are with coordinates', () => { + const params = { + ...validParams, + covenantNoCoordPks: validParams.covenantNoCoordPks.map(pk => '02' + pk ) + }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Covenant public key should contains no coordinate' + ); + }); + + it('should throw an error if covenant public keys are less than the quorum', () => { + const params = { ...validParams, covenantQuorum: validParams.covenantNoCoordPks.length + 1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Covenant public keys must be greater than or equal to the quorum' + ); + }); + + it('should throw an error if unbonding time is less than or equal to 0', () => { + let params = { ...validParams, unbondingTime: 0 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Unbonding time must be greater than 0' + ); + + params = { ...validParams, unbondingTime: -1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Unbonding time must be greater than 0' + ); + }); + + it('should throw an error if unbonding fee is less than or equal to 0', () => { + let params = { ...validParams, unbondingFeeSat: 0 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Unbonding fee must be greater than 0' + ); + + params = { ...validParams, unbondingFeeSat: -1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Unbonding fee must be greater than 0' + ); + }); + + it('should throw an error if max staking amount is less than min staking amount', () => { + const params = { ...validParams, maxStakingAmountSat: 500 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Max staking amount must be greater or equal to min staking amount' + ); + }); + + it('should throw an error if min staking amount is less than 1', () => { + const params = { ...validParams, minStakingAmountSat: -1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Min staking amount must be greater than unbonding fee plus 1000' + ); + + const params0 = { ...validParams, minStakingAmountSat: 0 }; + + expect(() => new Staking( + network, + stakerInfo, + params0, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Min staking amount must be greater than unbonding fee plus 1000' + ); + }); + + it('should throw an error if max staking time is less than min staking time', () => { + const params = { ...validParams, maxStakingTimeBlocks: validParams.minStakingTimeBlocks - 1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Max staking time must be greater or equal to min staking time' + ); + }); + + it('should throw an error if min staking time is less than 1', () => { + const params = { ...validParams, minStakingTimeBlocks: -1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Min staking time must be greater than 0' + ); + + const params0 = { ...validParams, minStakingTimeBlocks: 0 }; + + expect(() => new Staking( + network, + stakerInfo, + params0, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Min staking time must be greater than 0' + ); + }); + + it('should throw an error if covenant quorum is less than or equal to 0', () => { + let params = { ...validParams, covenantQuorum: 0 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Covenant quorum must be greater than 0' + ); + + params = { ...validParams, covenantQuorum: -1 }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Covenant quorum must be greater than 0' + ); + }); + + it('should throw an error if slashing rate is not within the range', () => { + const params0 = { ...validParams, slashing: { + ...validParams.slashing!, + slashingRate: 0, + } }; + + expect(() => new Staking( + network, + stakerInfo, + params0, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Slashing rate must be greater than 0' + ); + + const params1 = { ...validParams, slashing: { + ...validParams.slashing!, + slashingRate: 1.1, + } }; + + expect(() => new Staking( + network, + stakerInfo, + params1, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Slashing rate must be less or equal to 1' + ); + }); + + it('should throw an error if slashing public key scrit is empty', () => { + const params = { ...validParams, slashing: { + ...validParams.slashing!, + slashingPkScriptHex: "", + } }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Slashing public key script is missing' + ); + }); + + it('should throw an error if minSlashingTxFeeSat is not positive number', () => { + const params = { ...validParams, slashing: { + ...validParams.slashing!, + minSlashingTxFeeSat: 0, + } }; + + expect(() => new Staking( + network, + stakerInfo, + params, + finalityProviderPksNoCoordHex, + dataGenerator.generateRandomTimelock(validParams), + )).toThrow( + 'Minimum slashing transaction fee must be greater than 0' + ); + }); + }); +}); + diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts new file mode 100644 index 0000000000..b6555fab3b --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts @@ -0,0 +1,213 @@ +import { payments } from "bitcoinjs-lib"; +import { + getPublicKeyNoCoord, + isNativeSegwit, + isTaproot, + isValidNoCoordPublicKey, + transactionIdToHash, +} from '../../src/utils/btc'; +import { networks } from 'bitcoinjs-lib'; +import { testingNetworks } from '../helper'; +import { deriveStakingOutputInfo } from '../../src/utils/staking'; +import { Staking } from '../../src/staking'; + +describe('address type', () => { + describe.each(testingNetworks)('should return true for a valid address type', + ({ network, datagen: { stakingDatagen: dataGenerator } }) => { + const addresses = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey + ); + it('should return true for a valid Taproot address', () => { + expect(isTaproot(addresses.taproot.address, network)).toBe(true); + }); + + it('should return true for a valid Native SegWit address', () => { + expect(isNativeSegwit(addresses.nativeSegwit.address, network)).toBe(true); + }); + + it('should return false for non-Taproot address', () => { + expect(isTaproot(addresses.nativeSegwit.address, network)).toBe(false); + + const legacyAddress = '16o1TKSUWXy51oDpL5wbPxnezSGWC9rMPv'; + expect(isTaproot(legacyAddress, network)).toBe(false); + + const nestedSegWidth = '3A2yqzgfxwwqxgse5rDTCQ2qmxZhMnfd5b'; + expect(isTaproot(nestedSegWidth, network)).toBe(false); + }); + + it('should return false for non-Native SegWit address', () => { + expect(isNativeSegwit(addresses.taproot.address, network)).toBe(false); + + const legacyAddress = '16o1TKSUWXy51oDpL5wbPxnezSGWC9rMPv'; + expect(isNativeSegwit(legacyAddress, network)).toBe(false); + + const nestedSegWidth = '3A2yqzgfxwwqxgse5rDTCQ2qmxZhMnfd5b'; + expect(isNativeSegwit(nestedSegWidth, network)).toBe(false); + }); + + }); + + const [mainnetDatagen, signetDatagen] = testingNetworks; + const envNetworks = [ + { + mainnetDatagen: mainnetDatagen.datagen.stakingDatagen, + signetDatagen: signetDatagen.datagen.stakingDatagen, + }, + ]; + + envNetworks.forEach(({ mainnetDatagen, signetDatagen }) => { + const mainnetAddresses = mainnetDatagen.getAddressAndScriptPubKey( + mainnetDatagen.generateRandomKeyPair().publicKey + ); + const signetAddresses = signetDatagen.getAddressAndScriptPubKey( + signetDatagen.generateRandomKeyPair().publicKey + ); + + it('should return false for a mis-matched address type in different networks', () => { + expect(isTaproot(signetAddresses.nativeSegwit.address, networks.testnet)).toBe(false); + expect(isNativeSegwit(mainnetAddresses.taproot.address, networks.bitcoin)).toBe(false); + + const legacyAddress = 'n2eq5iP3UsdfmGsJyEEMXyRGNx5ysUXLXb'; + expect(isTaproot(legacyAddress, networks.testnet)).toBe(false); + expect(isNativeSegwit(legacyAddress, networks.bitcoin)).toBe(false); + + const nestedSegWidth = '2NChmRbq92M6geBmwCXcFF8dCfmGr38FmX2'; + expect(isTaproot(nestedSegWidth, networks.testnet)).toBe(false); + expect(isNativeSegwit(nestedSegWidth, networks.bitcoin)).toBe(false); + }); + + it('should return false for an invalid address format', () => { + const invalidAddress = 'invalid_address'; + expect(isTaproot(invalidAddress, networks.bitcoin)).toBe(false); + expect(isNativeSegwit(invalidAddress, networks.bitcoin)).toBe(false); + }); + + it('should return false for an incorrect network', () => { + expect(isTaproot(mainnetAddresses.taproot.address, networks.testnet)).toBe(false); + expect(isTaproot(mainnetAddresses.taproot.address, networks.regtest)).toBe(false); + + expect(isTaproot(signetAddresses.taproot.address, networks.bitcoin)).toBe(false); + + expect(isNativeSegwit(mainnetAddresses.nativeSegwit.address, networks.testnet)).toBe(false); + expect(isNativeSegwit(mainnetAddresses.nativeSegwit.address, networks.regtest)).toBe(false); + + expect(isNativeSegwit(signetAddresses.nativeSegwit.address, networks.bitcoin)).toBe(false); + }); + }); +}); + +describe.each(testingNetworks)('public keys', ({ datagen: { + stakingDatagen: dataGenerator +}}) => { + const { publicKey, publicKeyNoCoord } = dataGenerator.generateRandomKeyPair() + describe('isValidNoCoordPublicKey', () => { + it('should return true for a valid public key without a coordinate', () => { + expect(isValidNoCoordPublicKey(publicKeyNoCoord)).toBe(true); + }); + + it('should return false for a public key with a coordinate', () => { + expect(isValidNoCoordPublicKey(publicKey)).toBe(false); + }); + + it('should return false for an invalid public key', () => { + const invalidPublicKey = 'invalid_public_key'; + expect(isValidNoCoordPublicKey(invalidPublicKey)).toBe(false); + }); + }); + + describe('getPublicKeyNoCoord', () => { + it('should return the public key without the coordinate', () => { + expect(getPublicKeyNoCoord(publicKey)).toBe(publicKeyNoCoord); + }); + + it('should return the same public key without the coordinate', () => { + expect(getPublicKeyNoCoord(publicKeyNoCoord)).toBe(publicKeyNoCoord); + }); + + it('should throw an error for an invalid public key', () => { + const invalidPublicKey = 'invalid_public_key'; + expect(() => getPublicKeyNoCoord(invalidPublicKey)).toThrow('Invalid public key without coordinate'); + }); + }); +}); + +describe.each(testingNetworks)('Derive staking output address', ({ + network, + datagen: { + stakingDatagen: dataGenerator + } +}) => { + const feeRate = 1; + // Random number of finality providers + const finalityProviderPksNoCoordHex: string[] = []; + for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { + finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); + } + const { timelock, stakerInfo, params } = dataGenerator.generateRandomStakingTransaction( + network, feeRate + ); + + describe("should derive the staking output address from the scripts", () => { + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + const scripts = staking.buildScripts(); + const { outputAddress } = deriveStakingOutputInfo( + scripts, network + ); + expect(isTaproot(outputAddress, network)).toBe(true); + }); + + it("should throw an error if no address available from creation of pay-2-taproot output", () => { + jest.spyOn(payments, "p2tr").mockImplementation(() => { + return {}; + }); + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + const scripts = staking.buildScripts(); + expect(() => deriveStakingOutputInfo(scripts, network)) + .toThrow("Failed to build staking output"); + }); + + it("should throw an error if fail to create pay-2-taproot output", () => { + jest.spyOn(payments, "p2tr").mockImplementation(() => { + throw new Error("oops"); + }); + const staking = new Staking( + network, stakerInfo, + params, finalityProviderPksNoCoordHex, timelock, + ); + const scripts = staking.buildScripts(); + expect(() => deriveStakingOutputInfo(scripts, network)) + .toThrow("oops"); + }); +}); + +describe('transactionIdToHash', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + describe.each(testingNetworks)('should correctly convert transaction id to hash', + ({ datagen: { stakingDatagen: dataGenerator } }) => { + it('should correctly convert transaction id to hash', () => { + const utxos = dataGenerator.generateRandomUTXOs(1000, 5); // Generate multiple UTXOs to test + utxos.forEach(utxo => { + const txid = utxo.txid; + const expectedHash = Buffer.from(txid, 'hex').reverse(); + const result = transactionIdToHash(txid); + expect(Buffer.isBuffer(result)).toBe(true); + expect(result).toEqual(expectedHash); + expect(result.toString('hex')).toBe(expectedHash.toString('hex')); + }); + }); + + it('should throw an error if the transaction id is empty', () => { + const txId = ''; + expect(() => transactionIdToHash(txId)).toThrow("Transaction id cannot be empty"); + }); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts new file mode 100644 index 0000000000..f951d5e7ad --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts @@ -0,0 +1,276 @@ +import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; +import { UTXO } from "../../../src/types/UTXO"; +import { TransactionOutput } from "../../../src/types/psbtOutputs"; +import { getStakingExpansionTxFundingUTXOAndFees } from "../../../src/utils/fee"; +import { buildStakingTransactionOutputs, deriveStakingOutputInfo } from "../../../src/utils/staking"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; + +describe.each(testingNetworks)("utils - fee - ", ( + { networkName, network, datagen }, +) => { + describe(`${networkName} - getStakingExpansionTxFundingUTXOAndFees`, () => { + Object.entries(datagen).forEach(([_, dataGenerator]) => { + const mockScripts = dataGenerator.generateMockStakingScripts(); + const feeRate = DEFAULT_TEST_FEE_RATE; + + it("should throw an error if there are no available UTXOs", () => { + const availableUTXOs: UTXO[] = []; + const outputs: TransactionOutput[] = []; + expect(() => + getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + feeRate, + outputs, + ), + ).toThrow("Insufficient funds"); + }); + + it("should throw if no UTXO can cover the required fees", () => { + const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( + 100, + 2, + ); + const outputs = buildStakingTransactionOutputs(mockScripts, network, 50000); + expect(() => + getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + feeRate, + outputs, + ), + ).toThrow( + "Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction.", + ); + }); + + it("should successfully select the smallest UTXO that can cover the fee", () => { + const availableUTXOs: UTXO[] = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 1000000, // Large UTXO + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: 1, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 50000, // Medium UTXO that should be selected + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: 2, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 100000, // Larger UTXO + }, + ]; + const outputs = buildStakingTransactionOutputs(mockScripts, network, 50000); + + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + feeRate, + outputs, + ); + + // Should select the smallest UTXO that can cover the fee + expect(result.selectedUTXO).toEqual(availableUTXOs[1]); // The 50000 satoshi UTXO + expect(result.fee).toBeGreaterThan(0); + expect(result.fee).toBeLessThanOrEqual(result.selectedUTXO.value); + }); + + it("should successfully return the accurate fee for native segwit input", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 10000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + expect(result.fee).toBe(463); + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + + it("should successfully return the accurate fee for taproot input", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey({ + isTaproot: true, + }), + value: 10000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + expect(result.fee).toBe(453); + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + + it("should successfully return the accurate fee without change", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 420, // Just enough to cover fee without change + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + expect(result.fee).toBe(420); // Without change output, hence smaller + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + + it("should successfully return the fee without change when remaining balance is below dust threshold", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 420 + BTC_DUST_SAT, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + expect(result.fee).toBe(420); // Without change output, hence smaller + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + + it("should successfully return the accurate fee with change output", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 420 + BTC_DUST_SAT + 1, // More than dust threshold + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + expect(result.fee).toBe(463); + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + + it("should filter out invalid UTXOs with non-decompilable scripts", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: "invalid_script", // Invalid script + value: 10000, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: 1, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 10000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + + // Should select the valid UTXO + expect(result.selectedUTXO).toEqual(availableUTXOs[1]); + expect(result.fee).toEqual(463); + }); + + it("should throw error when no valid UTXOs are available", () => { + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: "invalid_script_1", // Invalid script + value: 10000, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: 1, + scriptPubKey: "invalid_script_2", // Invalid script + value: 10000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); + expect(() => + getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + feeRate, + outputs, + ), + ).toThrow("Insufficient funds: no valid UTXOs available for staking"); + }); + + it("should handle multiple outputs correctly in fee calculation", () => { + const availableUTXOs: UTXO[] = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + scriptPubKey: dataGenerator.generateRandomScriptPubKey(), + value: 10000, + }, + ]; + + // Create multiple outputs to test fee calculation + const stakingOutputInfo = deriveStakingOutputInfo(mockScripts, network); + const outputs: TransactionOutput[] = [ + { + scriptPubKey: stakingOutputInfo.scriptPubKey, + value: 2000, + }, + { + scriptPubKey: Buffer.from( + dataGenerator.generateRandomScriptPubKey(), "hex" + ), + value: 1000, + }, + ]; + + const result = getStakingExpansionTxFundingUTXOAndFees( + availableUTXOs, + 1, + outputs, + ); + expect(result.fee).toBe(506); + expect(result.selectedUTXO).toEqual(availableUTXOs[0]); + }); + }); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts new file mode 100644 index 0000000000..c8fc1c1441 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts @@ -0,0 +1,242 @@ +import { UTXO } from "../../../src/types/UTXO"; +import { TransactionOutput } from "../../../src/types/psbtOutputs"; +import { getStakingTxInputUTXOsAndFees } from "../../../src/utils/fee"; +import { buildStakingTransactionOutputs } from "../../../src/utils/staking"; +import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; + +describe.each(testingNetworks)("utils - fee - ", ( + { networkName, network, datagen }, +) => { + describe(`${networkName} - getStakingTxInputUTXOsAndFees`, () => { + Object.entries(datagen).forEach(([key, dataGenerator]) => { + const mockScripts = dataGenerator.generateMockStakingScripts(); + const feeRate = DEFAULT_TEST_FEE_RATE; + const randomAmount = Math.floor(Math.random() * 100000000) + 1000; + + it("should throw an error if there are no available UTXOs", () => { + const availableUTXOs: UTXO[] = []; + const outputs: TransactionOutput[] = []; + expect(() => + getStakingTxInputUTXOsAndFees( + availableUTXOs, + randomAmount, + feeRate, + outputs, + ), + ).toThrow("Insufficient funds"); + }); + + it("should throw if total utxos value can not cover the staking value + fee", () => { + const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( + randomAmount + 1, + Math.floor(Math.random() * 10) + 1, + ); + const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); + expect(() => + getStakingTxInputUTXOsAndFees( + availableUTXOs, + randomAmount, + feeRate, + outputs, + ), + ).toThrow( + "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees", + ); + }); + + it("should successfully select the correct UTXOs and calculate the fee", () => { + const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( + randomAmount + 10000000, // give enough satoshis to cover the fee + Math.floor(Math.random() * 10) + 1, + ); + const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); + + const result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + randomAmount, + feeRate, + outputs, + ); + // Ensure the correct UTXOs are selected + expect(result.selectedUTXOs.length).toBeLessThanOrEqual( + availableUTXOs.length, + ); + // Ensure the highest value UTXOs are selected + const sortedUTXOs = [...availableUTXOs].sort((a, b) => b.value - a.value); + expect(result.selectedUTXOs).toEqual( + sortedUTXOs.slice(0, result.selectedUTXOs.length), + ); + expect(result.fee).toBeGreaterThan(0); + }); + + it("should successfully return the accurate fee for taproot input", () => { + const stakeAmount = 2000; + const { taproot } = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: taproot.scriptPubKey, + value: 1000, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: taproot.scriptPubKey, + value: 2000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); + // Manually setting fee rate less than 2 so that the fee calculation included ESTIMATION_ACCUARACY_BUFFER + let result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 1, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(325); // This number is calculated manually + } else { + expect(result.fee).toBe(243); // staking has less fees due to no op_return + } + expect(result.selectedUTXOs.length).toEqual(2); + + result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 2, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(534); // This number is calculated manually + } else { + expect(result.fee).toBe(456); // staking has less fees due to no op_return + } + expect(result.selectedUTXOs.length).toEqual(2); + + // Once fee rate is set to 3, the fee will be calculated with addition of TX_BUFFER_SIZE_OVERHEAD * feeRate + result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 3, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(756); // This number is calculated manually + } else { + expect(result.fee).toBe(510); + } + expect(result.selectedUTXOs.length).toEqual(2); + }); + + it("should successfully return the accurate fee for native segwit input", () => { + const stakeAmount = 2000; + const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 1000, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 2000, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); + const result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 1, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(345); // This number is calculated manually + } else { + expect(result.fee).toBe(263); // staking has less fees due to no op_return + } + expect(result.selectedUTXOs.length).toEqual(2); + }); + + it("should successfully return the accurate fee without change", () => { + const stakeAmount = 2000; + const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 1009, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 1293, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); + const result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 1, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(302); // This is the fee for 2 inputs and 2 outputs without change + } else { + expect(result.fee).toBe(220); // staking has less fees due to no op_return + } + expect(result.selectedUTXOs.length).toEqual(2); + }); + + it("should successfully return the accurate fee utilising only one of the UTXOs", () => { + const stakeAmount = 2000; + const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( + dataGenerator.generateRandomKeyPair().publicKey, + ); + const availableUTXOs = [ + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 1000, + }, + { + txid: dataGenerator.generateRandomTxId(), + vout: Math.floor(Math.random() * 10), + scriptPubKey: nativeSegwit.scriptPubKey, + value: 2500, + }, + ]; + + const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); + const result = getStakingTxInputUTXOsAndFees( + availableUTXOs, + stakeAmount, + 1, + outputs, + ); + if (key === "observableStakingDatagen") { + expect(result.fee).toBe(234); + } else { + expect(result.fee).toBe(152); // staking has less fees due to no op_return + } + expect(result.selectedUTXOs.length).toEqual(1); + }); + }); + }); +}); + diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts new file mode 100644 index 0000000000..19bccffe75 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts @@ -0,0 +1,128 @@ +import { script as bitcoinScript, opcodes, payments } from "bitcoinjs-lib"; +import { + DEFAULT_INPUT_SIZE, + MAX_NON_LEGACY_OUTPUT_SIZE, + P2TR_INPUT_SIZE, + P2WPKH_INPUT_SIZE, +} from "../../../src/constants/fee"; +import { UTXO } from "../../../src/types/UTXO"; +import { + getEstimatedChangeOutputSize, + getInputSizeByScript, + inputValueSum, + isOP_RETURN, +} from "../../../src/utils/fee/utils"; +import { testingNetworks } from "../../helper"; + +describe("is OP_RETURN", () => { + it("should return true for an OP_RETURN script", () => { + const script = bitcoinScript.compile([ + opcodes.OP_RETURN, + Buffer.from("hello world"), + ]); + expect(isOP_RETURN(script)).toBe(true); + }); + + it("should return false for a non-OP_RETURN script", () => { + const script = bitcoinScript.compile([ + opcodes.OP_DUP, + opcodes.OP_HASH160, + Buffer.alloc(20), + opcodes.OP_EQUALVERIFY, + opcodes.OP_CHECKSIG, + ]); + expect(isOP_RETURN(script)).toBe(false); + }); + + it("should return false for an invalid script", () => { + const script = Buffer.from("invalidscript", "hex"); + expect(isOP_RETURN(script)).toBe(false); + }); +}); + +describe.each(testingNetworks)("scriptUtils", ({ + networkName, + datagen, +}) => { + describe.each(Object.values(datagen))(`${networkName} - getInputSizeByScript`, ( + dataGenerator + ) => { + it("should return P2WPKH_INPUT_SIZE for a valid P2WPKH script", () => { + const pk = dataGenerator.generateRandomKeyPair().publicKey; + const { output } = payments.p2wpkh({ pubkey: Buffer.from(pk, "hex") }); + if (output) { + expect(getInputSizeByScript(output)).toBe(P2WPKH_INPUT_SIZE); + } + }); + + it("should return P2TR_INPUT_SIZE for a valid P2TR script", () => { + const pk = dataGenerator.generateRandomKeyPair().publicKeyNoCoord; + const { output } = payments.p2tr({ + internalPubkey: Buffer.from(pk, "hex"), + }); + expect(getInputSizeByScript(output!)).toBe(P2TR_INPUT_SIZE); + }); + + it("should return DEFAULT_INPUT_SIZE for an invalid or unrecognized script", () => { + const script = bitcoinScript.compile([ + opcodes.OP_DUP, + opcodes.OP_HASH160, + Buffer.alloc(20), + opcodes.OP_EQUALVERIFY, + opcodes.OP_CHECKSIG, + ]); + expect(getInputSizeByScript(script)).toBe(DEFAULT_INPUT_SIZE); + }); + + it("should handle malformed scripts gracefully and return DEFAULT_INPUT_SIZE", () => { + const malformedScript = Buffer.from("00", "hex"); + expect(getInputSizeByScript(malformedScript)).toBe(DEFAULT_INPUT_SIZE); + }); + }); +}); + +describe("getEstimatedChangeOutputSize", () => { + it("should return correct value for the estimated change output size", () => { + expect(getEstimatedChangeOutputSize()).toBe(MAX_NON_LEGACY_OUTPUT_SIZE); + }); +}); + +describe("inputValueSum", () => { + it("should return the correct sum of UTXO values", () => { + const inputUTXOs: UTXO[] = [ + { txid: "txid1", vout: 0, value: 5000, scriptPubKey: "script1" }, + { txid: "txid2", vout: 1, value: 10000, scriptPubKey: "script2" }, + ]; + const expectedSum = 15000; + const actualSum = inputValueSum(inputUTXOs); + expect(actualSum).toBe(expectedSum); + }); + + it("should return zero for an empty UTXO list", () => { + const inputUTXOs: UTXO[] = []; + const expectedSum = 0; + const actualSum = inputValueSum(inputUTXOs); + expect(actualSum).toBe(expectedSum); + }); + + it("should return the correct sum for UTXOs with varying values", () => { + const inputUTXOs: UTXO[] = [ + { txid: "txid1", vout: 0, value: 2500, scriptPubKey: "script1" }, + { txid: "txid2", vout: 1, value: 7500, scriptPubKey: "script2" }, + { txid: "txid3", vout: 2, value: 10000, scriptPubKey: "script3" }, + ]; + const expectedSum = 20000; + const actualSum = inputValueSum(inputUTXOs); + expect(actualSum).toBe(expectedSum); + }); + + it("should handle large UTXO values correctly", () => { + const inputUTXOs: UTXO[] = [ + { txid: "txid1", vout: 0, value: 2 ** 53 - 1, scriptPubKey: "script1" }, + { txid: "txid2", vout: 1, value: 1, scriptPubKey: "script2" }, + ]; + const expectedSum = 2 ** 53 - 1 + 1; + const actualSum = inputValueSum(inputUTXOs); + expect(actualSum).toBe(expectedSum); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts new file mode 100644 index 0000000000..b050f26aa2 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts @@ -0,0 +1,26 @@ +import { + LOW_RATE_ESTIMATION_ACCURACY_BUFFER, + MAX_NON_LEGACY_OUTPUT_SIZE, + P2TR_INPUT_SIZE, + TX_BUFFER_SIZE_OVERHEAD, + WITHDRAW_TX_BUFFER_SIZE, +} from "../../../src/constants/fee"; +import { getWithdrawTxFee } from "../../../src/utils/fee"; + +describe("getWithdrawTxFee", () => { + it("should calculate the correct withdraw transaction fee for a given fee rate", () => { + const feeRate = Math.floor(Math.random() * 100); + let expectedTotalFee = + feeRate * + (P2TR_INPUT_SIZE + + MAX_NON_LEGACY_OUTPUT_SIZE + + WITHDRAW_TX_BUFFER_SIZE + + TX_BUFFER_SIZE_OVERHEAD); + if (feeRate <= 2) { + expectedTotalFee += LOW_RATE_ESTIMATION_ACCURACY_BUFFER; + } + const actualFee = getWithdrawTxFee(feeRate); + + expect(actualFee).toBe(expectedTotalFee); + }); +}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts new file mode 100644 index 0000000000..c6644618b0 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts @@ -0,0 +1,376 @@ +import { sha256 } from "bitcoinjs-lib/src/crypto"; + +import { + createStakerPopContext, + buildPopMessage, +} from "../../src/utils/pop"; +import { PopUpgradeConfig } from "../../src/types"; +import { STAKING_MODULE_ADDRESS } from "../../src/constants/staking"; +import { babylonAddress } from "../staking/manager/__mock__/fee"; +import { mockChainId } from "../staking/manager/__mock__/providers"; + +describe("POP Utility Functions", () => { + const mockBech32Address = babylonAddress; + + describe("createStakerPopContext", () => { + it("should generate correct context hash with default version", () => { + const contextHash = createStakerPopContext(mockChainId); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + + expect(contextHash).toBe(expectedHash); + }); + + it("should generate correct context hash with custom version", () => { + const contextHash = createStakerPopContext(mockChainId, 1); + + const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + + expect(contextHash).toBe(expectedHash); + }); + + it("should generate correct context hash with version 2", () => { + const contextHash = createStakerPopContext(mockChainId, 2); + + const expectedContextString = `btcstaking/2/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + + expect(contextHash).toBe(expectedHash); + }); + + it("should handle empty chain ID", () => { + const contextHash = createStakerPopContext(""); + + const expectedContextString = `btcstaking/0/staker_pop//${STAKING_MODULE_ADDRESS}`; + const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + + expect(contextHash).toBe(expectedHash); + }); + }); + + describe("buildPopMessage", () => { + const mockUpgradeConfig: PopUpgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + + describe("Legacy Format", () => { + it("should return bech32 address when no upgrade config provided", () => { + const currentHeight = 300; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + ); + + expect(result).toBe(mockBech32Address); + }); + + it("should return bech32 address when current height is below upgrade height", () => { + const currentHeight = 100; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + expect(result).toBe(mockBech32Address); + }); + + it("should return bech32 address when upgrade height is undefined", () => { + const result = buildPopMessage( + mockBech32Address, + 300, + mockChainId, + ); + + expect(result).toBe(mockBech32Address); + }); + }); + + describe("New Format", () => { + it("should return context hash + address when current height equals upgrade height", () => { + const currentHeight = 200; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should return context hash + address when current height is above upgrade height", () => { + const currentHeight = 300; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should use custom version when provided", () => { + const currentHeight = 300; + const customConfig = { + upgradeHeight: 200, + version: 1, + }; + + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + customConfig, + ); + + const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should always use new format when upgrade height is 0", () => { + const currentHeight = 100; + const customConfig: PopUpgradeConfig = { + upgradeHeight: 0, + version: 0, + }; + + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + customConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + }); + + describe("Edge Cases", () => { + it("should handle empty bech32 address", () => { + const currentHeight = 300; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + + const result = buildPopMessage( + "", + 300, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + ""; + + expect(result).toBe(expectedMessage); + }); + + it("should handle very large height values", () => { + const currentHeight = Number.MAX_SAFE_INTEGER; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should handle empty chain ID", () => { + const currentHeight = 300; + const upgradeConfig = { + upgradeHeight: mockUpgradeConfig.upgradeHeight, + version: mockUpgradeConfig.version, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + "", + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop//${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + }); + }); + + describe("buildPopMessage (createPopMessageToSign replacement)", () => { + describe("Success Cases", () => { + it("should return legacy format when no upgrade config provided", () => { + const currentHeight = 300; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + ); + + expect(result).toBe(mockBech32Address); + }); + + it("should return legacy format when height is below upgrade height", () => { + const currentHeight = 100; + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + expect(result).toBe(mockBech32Address); + }); + + it("should return new format when height is above upgrade height", () => { + const currentHeight = 300; + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should use custom version when provided", () => { + const currentHeight = 300; + const upgradeConfig = { + upgradeHeight: 200, + version: 1, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + }); + + describe("Edge Cases", () => { + it("should handle empty bech32 address", () => { + const currentHeight = 100; + const upgradeConfig = { + upgradeHeight: 200, + version: 0, + }; + const result = buildPopMessage( + "", + currentHeight, + mockChainId, + upgradeConfig, + ); + + expect(result).toBe(""); + }); + + it("should handle zero height", () => { + const currentHeight = 0; + const upgradeConfig: PopUpgradeConfig = { + upgradeHeight: 0, + version: 0, + }; + + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + // Should use new format since height (0) >= upgrade height (0) + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + + it("should handle very large height values", () => { + const currentHeight = Number.MAX_SAFE_INTEGER; + const upgradeConfig = { + upgradeHeight: 1000, + version: 0, + }; + const result = buildPopMessage( + mockBech32Address, + currentHeight, + mockChainId, + upgradeConfig, + ); + + const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; + const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); + const expectedMessage = expectedContextHash + mockBech32Address; + + expect(result).toBe(expectedMessage); + }); + }); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts new file mode 100644 index 0000000000..794c442b4c --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts @@ -0,0 +1,90 @@ +import { Transaction, address } from "bitcoinjs-lib"; +import { findMatchingTxOutputIndex } from "../../../src/utils/staking"; +import { testingNetworks } from "../../helper"; +import { StakingError, StakingErrorCode } from "../../../src/error"; + +describe.each(testingNetworks)('findMatchingTxOutputIndex', ( + { network, networkName, datagen: { stakingDatagen: dataGenerator } } +) => { + it(`${networkName} should find the correct output index for a valid address`, () => { + // Create a transaction with multiple outputs + const tx = new Transaction(); + const keyPair1 = dataGenerator.generateRandomKeyPair(); + const keyPair2 = dataGenerator.generateRandomKeyPair(); + const outputAddress1 = dataGenerator.getAddressAndScriptPubKey(keyPair1.publicKey).nativeSegwit.address; + const outputAddress2 = dataGenerator.getAddressAndScriptPubKey(keyPair2.publicKey).nativeSegwit.address; + + // Add outputs to the transaction + tx.addOutput( + address.toOutputScript(outputAddress1, network), + 1000000 + ); + tx.addOutput( + address.toOutputScript(outputAddress2, network), + 2000000 + ); + + // Test finding the first output + const index1 = findMatchingTxOutputIndex(tx, outputAddress1, network); + expect(index1).toBe(0); + + // Test finding the second output + const index2 = findMatchingTxOutputIndex(tx, outputAddress2, network); + expect(index2).toBe(1); + }); + + it(`${networkName} should throw an error when no matching output is found`, () => { + const tx = new Transaction(); + const keyPair1 = dataGenerator.generateRandomKeyPair(); + const keyPair2 = dataGenerator.generateRandomKeyPair(); + const outputAddress1 = dataGenerator.getAddressAndScriptPubKey(keyPair1.publicKey).nativeSegwit.address; + const outputAddress2 = dataGenerator.getAddressAndScriptPubKey(keyPair2.publicKey).nativeSegwit.address; + + // Add an output with a different address + tx.addOutput( + address.toOutputScript(outputAddress1, network), + 1000000 + ); + + // Try to find an address that doesn't exist in the outputs + expect(() => { + findMatchingTxOutputIndex(tx, outputAddress2, network); + }).toThrow(new StakingError( + StakingErrorCode.INVALID_OUTPUT, + `Matching output not found for address: ${outputAddress2}` + )); + }); + + it(`${networkName} should handle empty transaction outputs`, () => { + const tx = new Transaction(); + const keyPair = dataGenerator.generateRandomKeyPair(); + const outputAddress = dataGenerator.getAddressAndScriptPubKey(keyPair.publicKey).nativeSegwit.address; + + expect(() => { + findMatchingTxOutputIndex(tx, outputAddress, network); + }).toThrow(new StakingError( + StakingErrorCode.INVALID_OUTPUT, + `Matching output not found for address: ${outputAddress}` + )); + }); + + it(`${networkName} should handle no matching address from output scripts`, () => { + const tx = new Transaction(); + const keyPair = dataGenerator.generateRandomKeyPair(); + const outputAddress = dataGenerator.getAddressAndScriptPubKey( + keyPair.publicKey + ).nativeSegwit.address; + + tx.addOutput( + Buffer.from('OP_RETURN xyz'), + 1000000 + ); + + expect(() => { + findMatchingTxOutputIndex(tx, outputAddress, network); + }).toThrow(new StakingError( + StakingErrorCode.INVALID_OUTPUT, + `Matching output not found for address: ${outputAddress}` + )); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts new file mode 100644 index 0000000000..84ed4d1c85 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts @@ -0,0 +1,297 @@ +import { validateStakingExpansionCovenantQuorum, validateStakingExpansionInputs, validateStakingTxInputData } from "../../../src/utils/staking/validation"; +import { testingNetworks } from "../../helper"; + +describe.each(testingNetworks)('validateStakingTxInputData', ( + { datagen } +) => { + describe.each(Object.values(datagen))('validateStakingTxInputData', ( + dataGenerator + ) => { + const params = dataGenerator.generateStakingParams(); + const balance = dataGenerator.getRandomIntegerBetween( + params.maxStakingAmountSat, params.maxStakingAmountSat + 100000000, + ); + const numberOfUTXOs = dataGenerator.getRandomIntegerBetween(1, 10); + const validInputUTXOs = dataGenerator.generateRandomUTXOs(balance, numberOfUTXOs); + const feeRate = 1; + + it('should pass with valid staking amount, term, UTXOs, and fee rate', () => { + expect(() => + validateStakingTxInputData( + params.minStakingAmountSat, + params.minStakingTimeBlocks, + params, + validInputUTXOs, + feeRate, + ) + ).not.toThrow(); + }); + + it('should throw an error if staking amount is less than the minimum', () => { + expect(() => + validateStakingTxInputData( + params.minStakingAmountSat -1 , + params.minStakingTimeBlocks, + params, + validInputUTXOs, + feeRate, + ) + ).toThrow('Invalid staking amount'); + }); + + it('should throw an error if staking amount is greater than the maximum', () => { + expect(() => + validateStakingTxInputData( + params.maxStakingAmountSat + 1 , + params.minStakingTimeBlocks, + params, + validInputUTXOs, + feeRate, + ) + ).toThrow('Invalid staking amount'); + }); + + it('should throw an error if time lock is less than the minimum', () => { + expect(() => + validateStakingTxInputData( + params.maxStakingAmountSat, + params.minStakingTimeBlocks -1 , + params, + validInputUTXOs, + feeRate, + ) + ).toThrow('Invalid timelock'); + }); + + it('should throw an error if time lock is greater than the maximum', () => { + expect(() => + validateStakingTxInputData( + params.maxStakingAmountSat, + params.maxStakingTimeBlocks + 1 , + params, + validInputUTXOs, + feeRate, + ) + ).toThrow('Invalid timelock'); + }); + + it('should throw an error if no input UTXOs are provided', () => { + expect(() => + validateStakingTxInputData( + params.maxStakingAmountSat, + params.maxStakingTimeBlocks, + params, + [], + feeRate, + ) + ).toThrow('No input UTXOs provided'); + }); + + it('should throw an error if fee rate is less than or equal to zero', () => { + expect(() => + validateStakingTxInputData( + params.maxStakingAmountSat, + params.maxStakingTimeBlocks, + params, + validInputUTXOs, + 0, + ) + ).toThrow('Invalid fee rate'); + }); + }); +}); + +describe("validateStakingExpansionInputs", () => { + const [mainnet] = testingNetworks; + const { datagen: { stakingDatagen }, network } = mainnet; + + const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; + const stakerKeyPair = stakingDatagen.generateRandomKeyPair(); + const { + stakingAmountSat, + finalityProviderPksNoCoordHex, + timelock, + utxos + } = stakingDatagen.generateRandomStakingTransaction(network, 1, stakerKeyPair); + + it("should pass with valid staking expansion inputs", () => { + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 1, + inputUTXOs: utxos, + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex: finalityProviderPksNoCoordHex.slice(0, 1), + stakingAmountSat, + stakingTimelock: timelock, + }, + babylonAddress: babylonAddress, + }) + ).not.toThrow(); + }); + + it("should throw an error if the previous staking has more finality providers than the current staking", () => { + const extraFp = stakingDatagen.generateRandomFidelityProviderPksNoCoordHex(1); + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 1, + inputUTXOs: utxos, + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex: [ + ...finalityProviderPksNoCoordHex, + ...extraFp, + ], + stakingAmountSat, + stakingTimelock: timelock, + }, + babylonAddress: babylonAddress, + }) + ).toThrow( + `Invalid staking expansion: all finality providers from the previous + staking must be included. Missing: ${extraFp.join(", ")}`, + ); + }); + + it("should throw an error if the babylon address is invalid", () => { + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 1, + inputUTXOs: utxos, + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + babylonAddress: "invalid", + }) + ).toThrow("Invalid Babylon address"); + }); + + it("should throw an error if the babylon BTC tip height is 0", () => { + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 0, + inputUTXOs: utxos, + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + }) + ).toThrow("Babylon BTC tip height cannot be 0"); + }); + + it("should throw an error if no input UTXOs are provided", () => { + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 1, + inputUTXOs: [], + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + babylonAddress: babylonAddress, + }) + ).toThrow("No input UTXOs provided"); + }); + + it("should throw an error if the staking amount is not equal to the previous staking amount", () => { + expect(() => + validateStakingExpansionInputs({ + babylonBtcTipHeight: 1, + inputUTXOs: utxos, + stakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat: stakingAmountSat - 1, + stakingTimelock: timelock, + }, + previousStakingInput: { + finalityProviderPksNoCoordHex, + stakingAmountSat, + stakingTimelock: timelock, + }, + babylonAddress: babylonAddress, + }) + ).toThrow("Staking expansion amount must equal the previous staking amount"); + }); +}); + +describe.each(testingNetworks)("validateStakingExpansionCovenantQuorum", ( + { datagen: { stakingDatagen } } +) => { + const previousParams = stakingDatagen.generateStakingParams(true, 10); + const { + covenantQuorum: requiredQuorum, + } = previousParams; + + it("should pass with valid same staking parameters", () => { + expect(() => + validateStakingExpansionCovenantQuorum(previousParams, previousParams) + ).not.toThrow(); + }); + + it("should throw an error if the previous staking has less covenant members than the required quorum", () => { + // Replace the previous covenant members with new ones. + // The replaced number of members matches the quorum. + const newParams = JSON.parse(JSON.stringify(previousParams)); + const newCovenantNoCoordPks = stakingDatagen.generateRandomCovenantCommittee(requiredQuorum).map( + (buffer) => buffer.toString("hex"), + ); + const { covenantNoCoordPks } = newParams; + + newParams.covenantNoCoordPks = covenantNoCoordPks.slice( + requiredQuorum, covenantNoCoordPks.length + ).concat(newCovenantNoCoordPks); + + expect(() => + validateStakingExpansionCovenantQuorum(previousParams, newParams) + ).toThrow( + `Staking expansion failed: insufficient covenant quorum. ` + + `Required: ${requiredQuorum}, Available: ${covenantNoCoordPks.length - requiredQuorum}. ` + + `Too many covenant members have rotated out.` + ); + }); + + it("should pass with number of rotated out covenant members less than the required quorum", () => { + // Replace the previous covenant members with new ones. + // The replaced number of members matches the quorum. + const newParams = JSON.parse(JSON.stringify(previousParams)); + const newCovenantNoCoordPks = stakingDatagen.generateRandomCovenantCommittee(requiredQuorum-1).map( + (buffer) => buffer.toString("hex"), + ); + const { covenantNoCoordPks } = newParams; + + newParams.covenantNoCoordPks = covenantNoCoordPks.slice( + 0, requiredQuorum + ).concat(newCovenantNoCoordPks); + + expect(() => + validateStakingExpansionCovenantQuorum(previousParams, newParams) + ).not.toThrow(); + }); +}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts new file mode 100644 index 0000000000..d697addc1a --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts @@ -0,0 +1,259 @@ +import * as bitcoin from "bitcoinjs-lib"; + +import { UTXO } from "../../../src/types"; +import { getPsbtInputFields } from "../../../src/utils/utxo/getPsbtInputFields"; +import * as getScriptType from "../../../src/utils/utxo/getScriptType"; +import testingNetworks from "../../helper/testingNetworks"; + +describe.each(testingNetworks)( + "Get PSBT input fields for UTXOs", + ({ network, datagen }) => { + describe.each(Object.entries(datagen))( + "using %s", + (_dataGenName, dataGenerator) => { + const dummyRawTxHex = "0200000001abcdef"; + const dummyRedeemScriptHex = "abcdef"; + const dummyValue = 10000 + + // helper for this particular tests + function makeUTXO( + scriptPubKey: string, + overrides?: Partial, + ): UTXO { + return { + txid: dataGenerator.generateRandomTxId(), + vout: 0, + value: dummyValue, + scriptPubKey, + ...overrides, + }; + } + + // this will throw from `getScriptType` + it("throws if cannot get the script type", () => { + const unknownScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + Buffer.from("UNKNOWN"), + ]); + + const utxo = makeUTXO(unknownScript.toString("hex")); + expect(() => getPsbtInputFields(utxo)).toThrow("Unknown script type"); + }); + + // this will throw from `getPsbtInputField` + it("throws if the script type is not supported", () => { + const unknownScript = "NOT_SUPPORTED_SCRIPT"; + + // Save the spy so we can restore it + const spy = jest + .spyOn(getScriptType, "getScriptType") + .mockImplementation(() => { + return unknownScript as any; + }); + + try { + const notSupportedScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + Buffer.from("UNKNOWN"), + ]); + const utxo = makeUTXO(notSupportedScript.toString("hex")); + expect(() => getPsbtInputFields(utxo)).toThrow( + `Unsupported script type: ${unknownScript}`, + ); + } finally { + // Restore the original implementation, otherwise it will affect other tests + spy.mockRestore(); + } + }); + + describe("P2PKH", () => { + it("returns nonWitnessUtxo when rawTxHex is provided", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const p2pkh = bitcoin.payments.p2pkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + // Must include rawTxHex for legacy inputs + const utxo = makeUTXO(p2pkh.output!.toString("hex"), { + rawTxHex: dummyRawTxHex, + }); + + const fields = getPsbtInputFields(utxo); + expect(fields.nonWitnessUtxo).toEqual( + Buffer.from(dummyRawTxHex, "hex"), + ); + // Should not have witnessUtxo, redeemScript, or witnessScript + expect(fields.witnessUtxo).toBeUndefined(); + expect(fields.redeemScript).toBeUndefined(); + expect(fields.witnessScript).toBeUndefined(); + }); + + it("throws an error if rawTxHex is missing for P2PKH", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const p2pkh = bitcoin.payments.p2pkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + const utxo = makeUTXO(p2pkh.output!.toString("hex")); + expect(() => getPsbtInputFields(utxo)).toThrow( + "Missing rawTxHex for legacy P2PKH input", + ); + }); + }); + + describe("P2SH", () => { + it("returns nonWitnessUtxo and redeemScript for valid P2SH", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const nested = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); + + const utxo = makeUTXO(p2sh.output!.toString("hex"), { + rawTxHex: dummyRawTxHex, + redeemScript: dummyRedeemScriptHex, + }); + const fields = getPsbtInputFields(utxo); + expect(fields.nonWitnessUtxo).toEqual( + Buffer.from(dummyRawTxHex, "hex"), + ); + expect(fields.redeemScript).toEqual( + Buffer.from(dummyRedeemScriptHex, "hex"), + ); + expect(fields.witnessUtxo).toBeUndefined(); + }); + + it("throws if rawTxHex is missing for P2SH", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const nested = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); + + const utxo = makeUTXO(p2sh.output!.toString("hex"), { + // No rawTxHex + redeemScript: dummyRedeemScriptHex, + }); + expect(() => getPsbtInputFields(utxo)).toThrow( + "Missing rawTxHex for P2SH input", + ); + }); + + it("throws if redeemScript is missing for P2SH", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const nested = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); + + const utxo = makeUTXO(p2sh.output!.toString("hex"), { + rawTxHex: dummyRawTxHex, + // No redeemScript + }); + expect(() => getPsbtInputFields(utxo)).toThrow( + "Missing redeemScript for P2SH input", + ); + }); + }); + + describe("P2WPKH", () => { + it("returns witnessUtxo only for valid P2WPKH", () => { + const { publicKey } = dataGenerator.generateRandomKeyPair(); + const p2wpkh = bitcoin.payments.p2wpkh({ + pubkey: Buffer.from(publicKey, "hex"), + network, + }); + const utxo = makeUTXO(p2wpkh.output!.toString("hex")); + + const fields = getPsbtInputFields(utxo); + expect(fields.witnessUtxo).toBeDefined(); + expect(fields.witnessUtxo?.script).toEqual( + Buffer.from(p2wpkh.output!.toString("hex"), "hex"), + ); + expect(fields.witnessUtxo?.value).toBe(dummyValue); + expect(fields.nonWitnessUtxo).toBeUndefined(); + expect(fields.redeemScript).toBeUndefined(); + expect(fields.witnessScript).toBeUndefined(); + }); + }); + + describe("P2WSH", () => { + it("returns witnessUtxo and witnessScript for valid P2WSH with custom script", () => { + const customScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + Buffer.from("hello"), + ]); + const p2wsh = bitcoin.payments.p2wsh({ + redeem: { output: customScript }, + network, + }); + + const utxo = makeUTXO(p2wsh.output!.toString("hex"), { + witnessScript: customScript.toString("hex"), + }); + + const fields = getPsbtInputFields(utxo); + expect(fields.witnessUtxo).toEqual({ + script: Buffer.from(p2wsh.output!.toString("hex"), "hex"), + value: dummyValue, + }); + expect(fields.witnessScript).toEqual( + Buffer.from(customScript.toString("hex"), "hex"), + ); + expect(fields.nonWitnessUtxo).toBeUndefined(); + expect(fields.redeemScript).toBeUndefined(); + }); + + it("throws if witnessScript is missing for P2WSH", () => { + const customScript = bitcoin.script.compile([ + bitcoin.opcodes.OP_RETURN, + Buffer.from("hello"), + ]); + const p2wsh = bitcoin.payments.p2wsh({ + redeem: { output: customScript }, + network, + }); + const utxo = makeUTXO(p2wsh.output!.toString("hex")); // no witnessScript + + expect(() => getPsbtInputFields(utxo)).toThrow( + "Missing witnessScript for P2WSH input", + ); + }); + }); + + describe("P2TR (Taproot)", () => { + it("returns witnessUtxo only if no publicKeyNoCoord is passed", () => { + const kp = dataGenerator.generateRandomKeyPair(); + const noCoord = Buffer.from(kp.publicKeyNoCoord, "hex"); + const p2tr = bitcoin.payments.p2tr({ + internalPubkey: noCoord, + network, + }); + const utxo = makeUTXO(p2tr.output!.toString("hex")); + + const fields = getPsbtInputFields(utxo); + expect(fields.witnessUtxo).toBeDefined(); + expect(fields.tapInternalKey).toBeUndefined(); + }); + + it("returns tapInternalKey if publicKeyNoCoord is passed", () => { + const kp = dataGenerator.generateRandomKeyPair(); + const noCoord = Buffer.from(kp.publicKeyNoCoord, "hex"); + const p2tr = bitcoin.payments.p2tr({ + internalPubkey: noCoord, + network, + }); + const utxo = makeUTXO(p2tr.output!.toString("hex")); + + const fields = getPsbtInputFields(utxo, noCoord); + expect(fields.witnessUtxo).toBeDefined(); + expect(fields.tapInternalKey).toEqual(noCoord); + }); + }); + }, + ); + }, +); From adc65a89efb670de190736b0e1a794ab76e2df8d Mon Sep 17 00:00:00 2001 From: Ben Kelcher Date: Thu, 2 Oct 2025 08:22:06 -0400 Subject: [PATCH 02/10] chore: remove dev and test files This commit removes unnecessary development and test files from the vendor directory. Ticket: SC-3362 --- .../.eslintrc.json | 31 -- .../.github/workflows/ci.yml | 14 - .../.github/workflows/manual-publish.yaml | 20 - .../.github/workflows/publish.yaml | 18 - .../.husky/commit-msg | 7 - .../.husky/pre-commit | 2 - modules/babylonlabs-io-btc-staking-ts/.npmrc | 1 - modules/babylonlabs-io-btc-staking-ts/.nvmrc | 1 - .../.prettierignore | 10 - .../.prettierrc.json | 9 - .../.releaserc.json | 11 - .../babylonlabs-io-btc-staking-ts/README.md | 57 -- .../docs/advanced-btc-tx.md | 491 ----------------- .../docs/usage.md | 517 ------------------ .../tests/helper/datagen/base.ts | 488 ----------------- .../tests/helper/datagen/observable.ts | 43 -- .../tests/helper/index.ts | 2 - .../tests/helper/math.ts | 30 - .../tests/helper/testingNetworks.ts | 30 - .../staking/createCovenantWitness.test.ts | 169 ------ .../tests/staking/createSlashingTx.test.ts | 145 ----- .../tests/staking/createStakingTx.test.ts | 248 --------- .../tests/staking/createUnbondingtx.test.ts | 102 ---- .../tests/staking/createWithdrawTx.test.ts | 112 ---- .../tests/staking/manager/__mock__/fee.ts | 233 -------- .../staking/manager/__mock__/providers.ts | 13 - .../staking/manager/__mock__/registration.ts | 494 ----------------- .../tests/staking/manager/__mock__/staking.ts | 114 ---- .../staking/manager/__mock__/unbonding.ts | 145 ----- .../staking/manager/__mock__/withdrawal.ts | 129 ----- .../tests/staking/manager/fee.test.ts | 81 --- .../tests/staking/manager/init.test.ts | 33 -- .../tests/staking/manager/pop.test.ts | 226 -------- .../tests/staking/manager/postStaking.test.ts | 211 ------- .../tests/staking/manager/preStaking.test.ts | 228 -------- .../tests/staking/manager/staking.test.ts | 112 ---- .../tests/staking/manager/unbonding.test.ts | 172 ------ .../tests/staking/manager/withdrawal.test.ts | 208 ------- .../observable/createStakingTx.test.ts | 162 ------ .../observableStakingScript.test.ts | 152 ----- .../staking/observable/validation.test.ts | 78 --- .../staking/psbt/stakingExpansionPsbt.test.ts | 464 ---------------- .../tests/staking/psbt/stakingPsbt.test.ts | 183 ------- .../tests/staking/psbt/unbondingPsbt.test.ts | 119 ---- .../tests/staking/stakingScript.test.ts | 415 -------------- .../transactions/slashingTransaction.test.ts | 360 ------------ .../stakingExpansionTransaction.test.ts | 200 ------- .../transactions/stakingTransaction.test.ts | 381 ------------- .../transactions/unbondingTransaction.test.ts | 86 --- .../transactions/withdrawTransaction.test.ts | 326 ----------- .../tests/staking/validation.test.ts | 367 ------------- .../tests/utils/btc.test.ts | 213 -------- .../utils/fee/stakingExpansionTxFee.test.ts | 276 ---------- .../tests/utils/fee/stakingtxFee.test.ts | 242 -------- .../tests/utils/fee/utils.test.ts | 128 ----- .../tests/utils/fee/withdrawTxFee.test.ts | 26 - .../tests/utils/pop.test.ts | 376 ------------- .../staking/findMatchingTxOutputIndex.test.ts | 90 --- .../tests/utils/staking/validation.test.ts | 297 ---------- .../utils/utxo/getPsbtInputFields.test.ts | 259 --------- 60 files changed, 10157 deletions(-) delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.eslintrc.json delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.npmrc delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.nvmrc delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.prettierignore delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.prettierrc.json delete mode 100644 modules/babylonlabs-io-btc-staking-ts/.releaserc.json delete mode 100644 modules/babylonlabs-io-btc-staking-ts/README.md delete mode 100644 modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md delete mode 100644 modules/babylonlabs-io-btc-staking-ts/docs/usage.md delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts diff --git a/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json b/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json deleted file mode 100644 index 943cb53e4d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.eslintrc.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "parserOptions": { - "sourceType": "module", - "ecmaVersion": "latest" - }, - "overrides": [ - { - "files": [ - "tests/**/*.ts" - ], - "parser": "@typescript-eslint/parser", - "rules": { - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-explicit-any": "off" - } - }, - { - "files": [ - "jest.setup.js" - ], - "rules": { - "no-undef": "off" - } - }, - ] -} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml deleted file mode 100644 index d1dd9bdbb7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/ci.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: ci - -on: - pull_request: - branches: - - "**" - -jobs: - lint_test: - uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.9.0 - with: - run-build: true - run-unit-tests: true - node-version: 24.2.0 \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml deleted file mode 100644 index 9b95a3c66d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/manual-publish.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Manual release branch release - -on: - workflow_dispatch: - push: - branches: - - 'release/v[0-9]+.[0-9]+.[0-9]+' -permissions: - contents: write -jobs: - lint_test: - uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.13.1 - secrets: inherit - with: - run-build: true - run-unit-tests: true - publish: true - publish-command: | - ./bin/ci_validate_version.sh - npm publish diff --git a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml b/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml deleted file mode 100644 index 88a5643ed3..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.github/workflows/publish.yaml +++ /dev/null @@ -1,18 +0,0 @@ -name: Semantic release - -on: - workflow_dispatch: - push: - branches: - - main -permissions: - contents: write -jobs: - lint_test: - uses: babylonlabs-io/.github/.github/workflows/reusable_node_lint_test.yml@v0.13.1 - secrets: inherit - with: - run-build: true - run-unit-tests: true - use-semantic-release: true - node-version: 24.2.0 diff --git a/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg b/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg deleted file mode 100644 index 3629fd94ab..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.husky/commit-msg +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -# Only run commitlint if a commit message file was passed in -if [ -n "$1" ] && [ -f "$1" ]; then - npx --no-install commitlint --edit "$1" -fi \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit b/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit deleted file mode 100644 index 045cbbdb56..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -TEST_REPEAT_TIMES=5 npm test -npm run build \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.npmrc b/modules/babylonlabs-io-btc-staking-ts/.npmrc deleted file mode 100644 index 4fd021952d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.nvmrc b/modules/babylonlabs-io-btc-staking-ts/.nvmrc deleted file mode 100644 index 4bbfbca25c..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v24.2.0 \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.prettierignore b/modules/babylonlabs-io-btc-staking-ts/.prettierignore deleted file mode 100644 index c16757f2e0..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -dist -build -coverage -.circleci -.husky -.next -docs -public -README.md \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json b/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json deleted file mode 100644 index a07eac9270..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "semi": true, - "singleQuote": false, - "tabWidth": 2, - "useTabs": false, - "plugins": [ - "prettier-plugin-organize-imports" - ] -} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/.releaserc.json b/modules/babylonlabs-io-btc-staking-ts/.releaserc.json deleted file mode 100644 index 2a963bf561..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/.releaserc.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "branches": [ - "main" - ], - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/npm", - "@semantic-release/github" - ] -} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/README.md b/modules/babylonlabs-io-btc-staking-ts/README.md deleted file mode 100644 index 5d5c743a7f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/README.md +++ /dev/null @@ -1,57 +0,0 @@ -

- Babylon Logo -

@babylonlabs-io/btc-staking-ts

-

Babylon Bitcoin Staking Protocol

-

TypeScript library

-

- npm version -

-

-
- -## 👨🏻‍💻 Installation - -```console -npm i @babylonlabs-io/btc-staking-ts -``` - -## 📝 Commit Format & Automated Releases - -This project uses [**Conventional Commits**](https://www.conventionalcommits.org/en/v1.0.0/) -and [**semantic-release**](https://semantic-release.gitbook.io/) to automate -versioning, changelog generation, and npm publishing. -However, release branch will be cut wiht the syntax of `release/vY.X` whenever there is a major version bump. - -### ✅ How It Works - -1. All commits must follow the **Conventional Commits** format. -2. When changes are merged into the `main` branch: - - `semantic-release` analyzes commit messages - - Determines the appropriate semantic version bump (`major`, `minor`, `patch`) - - Updates the `CHANGELOG.md` - - Tags the release in Git - - Publishes the new version to npm (if configured) - -### 🧱 Commit Message Examples - -```console -feat: add support for slashing script -fix: handle invalid staking tx gracefully -docs: update README with commit conventions -refactor!: remove deprecated method and cleanup types -``` - -> **Note:** For breaking changes, add a `!` after the type ( -> e.g. `feat!:` or `refactor!:`) and include a description of the breaking -> change in the commit body. - -### 🚀 Releasing - -Just commit your changes using the proper format and merge to `main`. -The CI pipeline will handle versioning and releasing automatically — no manual -tagging or version bumps needed. - -## 📢 Usage Guide - -Details on the usage of the library can be found -on the [usage guide](./docs/usage.md). diff --git a/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md b/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md deleted file mode 100644 index f952be2a22..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/docs/advanced-btc-tx.md +++ /dev/null @@ -1,491 +0,0 @@ -# Advanced BTC Staking Transaction Usage - -> ⚠️ **WARNING**: This documentation describes advanced usage of btc-staking-ts -> where you can customize transaction parameters to fit your specific needs. -> While this offers more flexibility, creating custom Bitcoin transactions -> carries inherent risks. Incorrect parameters or improper usage could result -> in loss of funds. Proceed at your own risk and thoroughly test all transactions -> in a test environment first. - - -This guide demonstrates how to manually construct Bitcoin staking transactions by customizing various parameters such as covenant settings, staking durations, and transaction details. It's intended for developers who need fine-grained control over the Bitcoin transaction aspect of the staking process. It does not cover the Babylon transaction aspect. - - -## Advanced Usage - -### Define Staking Parameters - -To determine the correct parameter version to use, this library provides two utility methods: -- `getBabylonParamByBtcHeight`: Get parameters based on Bitcoin block height -- `getBabylonParamByVersion`: Get parameters based on version number - -These methods ensure you use the appropriate parameter set based on the current state of the Babylon network. - -```ts -import { networks } from "bitcoinjs-lib"; - -// 1. Collect the Babylon system parameters. -// These are parameters that are shared between for all Bitcoin staking -// transactions, and are maintained by Babylon governance. -// They involve: -// - `covenantPks: Buffer[]`: A list of the public keys -// without the coordinate bytes correspondongin to the -// covenant emulators. -// - `covenantThreshold: number`: The amount of covenant -// emulator signatures required for the staking to be activated. -// - `minimumUnbondingTime: number`: The minimum unbonding period -// allowed by the Babylon system . -// - `lockHeight: number`: Indicates the BTC height before which -// the transaction is considered invalid. This value can be derived from -// the `activationHeight` of the Babylon versioned global parameters -// where the current BTC height is. Note that if the -// `current BTC height + 1 + confirmationDepth` is going to be >= -// the next versioned `activationHeight`, then you should use the -// `activationHeight` from the next version of the global parameters. -// Below, these values are hardcoded, but they should be retrieved from the -// Babylon system. -const covenantPks: Buffer[] = covenant_pks.map((pk) => Buffer.from(pk, "hex")); -const covenantThreshold: number = 3; -const minUnbondingTime: number = 101; -// Optional field. Value coming from current global param activationHeight -const lockHeight: number = 0; - -// 2. Define the user selected parameters of the staking contract: -// - `stakerPk: Buffer`: The public key without the coordinate of the -// staker. -// - `finalityProviders: Buffer[]`: A list of public keys without the -// coordinate corresponding to the finality providers. Currently, -// a delegation to only a single finality provider is allowed, -// so the list should contain only a single item. -// - `stakingDuration: number`: The staking period in BTC blocks. -// - `stakingAmount: number`: The amount to be staked in satoshis. -// - `unbondingTime: number`: The unbonding time. Should be `>=` the -// `minUnbondingTime`. - -const stakerPk: Buffer = btcWallet.publicKeyNoCoord(); -const finalityProviders: Buffer[] = [ - Buffer.from(finalityProvider.btc_pk_hex, "hex"), -]; -const stakingDuration: number = 144; -const stakingAmount: number = 1000; -const unbondingTime: number = minUnbondingTime; - -// 3. Define the parameters for the staking transaction that will contain the -// staking contract: -// - `inputUTXOs: UTXO[]`: The list of UTXOs that will be used as an input -// to fund the staking transaction. -// - `feeRate: number`: The fee per tx byte in satoshis. -// - `changeAddress: string`: BTC wallet change address, Taproot or Native -// Segwit. -// - `network: network to work with, either networks.testnet -// for BTC Testnet and BTC Signet, or networks.bitcoin for BTC Mainnet. - -// Each object in the inputUTXOs array represents a single UTXO with the following properties: -// - txid: transaction ID, string -// - vout: output index, number -// - value: value of the UTXO, in satoshis, number -// - scriptPubKey: script which provides the conditions that must be fulfilled for this UTXO to be spent, string -const inputUTXOs = [ - { - txid: "e472d65b0c9c1bac9ffe53708007e57ab830f1bf09af4bfbd17e780b641258fc", - vout: 2, - value: 9265692, - scriptPubKey: "0014505049839bc32f869590adc5650c584e17c917fc", - }, -]; -const feeRate: number = 18; -const changeAddress: string = btcWallet.address; -const network = networks.testnet; -``` - -### Create the Staking Contract - -After defining its parameters, -the staking contract can be created. -First, create an instance of the `StakingScriptData` class -and construct the Bitcoin scipts associated with Bitcoin staking using it. - -```ts -import { StakingScriptData } from "@babylonlabs-io/btc-staking-ts"; - -const stakingScriptData = new StakingScriptData( - stakerPk, - finalityProviders, - covenantPks, - covenantThreshold, - stakingDuration, - minUnbondingTime -); - -const { - timelockScript, - unbondingScript, - slashingScript, - unbondingTimelockScript, -} = stakingScriptData.buildScripts(); -``` - -The above scripts correspond to the following: - -- `timelockScript`: A script that allows the Bitcoin to be retrieved only - through the staker's signature and the staking period being expired. -- `unbondingScript`: The script that allows on-demand unbonding. - Requires the staker's signature and the covenant committee's signatures. -- `slashingScript`: The script that enables slashing. - It requires the staker's signature and in this phase the staker should not sign it. - -### Create a staking transaction - -Using the Bitcoin staking scripts, you can generate a Bitcoin staking -transaction and later sign it using a supported wallet's method. -In this instance, we use the `btcWallet.signTransaction()` method. - -```ts -import { stakingTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction } from "bitcoinjs-lib"; - -// stakingTransaction constructs an unsigned BTC Staking transaction -const unsignedStakingPsbt: {psbt: Psbt, fee: number} = stakingTransaction( - scripts: { - timelockScript, - unbondingScript, - slashingScript, - }, - stakingAmount, - changeAddress, - inputUTXOs, - network(), - feeRate, - btcWallet.isTaproot ? btcWallet.publicKeyNoCoord() : undefined, - lockHeight, -); - -const signedStakingPsbt = await btcWallet.signPsbt(unsignedStakingPsbt.psbt.toHex()); -const stakingTx = Psbt.fromHex(signedStakingPsbt).extractTransaction(); -``` - -Public key is needed only if the wallet is in Taproot mode, for `tapInternalKey`. - -### Create staking expansion transaction - -Staking expansion allows you to extend an existing BTC stake with additional -finality providers or renew the timelock without going through the full -unbonding process. - -The expansion transaction: -1. Spends the previous staking transaction output as the first input. -2. Uses a funding UTXO as the second input to cover transaction fees. -3. Creates new staking outputs where the timelock is renewed or additional -finality providers are added. -4. Returns any remaining funds as change. - -```ts -import { stakingExpansionTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction } from "bitcoinjs-lib"; - -// Previous staking transaction that we want to expand -const previousStakingTx: Transaction = stakingTx; // from previous staking - -// Scripts from the previous staking transaction -const previousStakingScripts = { - timelockScript, - unbondingScript, - slashingScript, -}; - -// New scripts for the expansion. The finality providers of this scripts must -// include ALL finality providers from the previous staking transaction. -// Additional finality providers can be added, but none can be removed. -// The timelock script can be renewed or left unchanged. -const expansionScripts = { - timelockScript: newTimelockScript, - unbondingScript: newUnbondingScript, - slashingScript: newSlashingScript, -}; - -// Funding UTXOs to cover transaction fees. -// The funding UTXOs are used only to cover transaction fees, not to increase -// the staking amount (this feature is not yet supported). -// Any remaining funds from the funding UTXOs will be returned as change. -// The method automatically selects the funding UTXO that can cover the -// transaction fees, so a single funding UTXO is sufficient. -const fundingUTXOs = [ - { - txid: "e472d65b0c9c1bac9ffe53708007e57ab830f1bf09af4bfbd17e780b641258fc", - vout: 2, - value: 9265692, - scriptPubKey: "0014505049839bc32f869590adc5650c584e17c917fc", - }, -]; - -const expansionResult = stakingExpansionTransaction( - network, - expansionScripts, - stakingAmount, // Must equal the previous staking amount - changeAddress, - feeRate, - fundingUTXOs, - { - stakingTx: previousStakingTx, - scripts: previousStakingScripts, - } -); - -const { - transaction: stakingExpansionTx, - fee: expansionFee, - fundingUTXO, // The selected funding UTXO that covers the transaction fees -} = expansionResult; - -// Sign the expansion transaction -const signedExpansionPsbt = await btcWallet.signPsbt(stakingExpansionTx.toHex()); -const signedExpansionTx = Psbt.fromHex(signedExpansionPsbt).extractTransaction(); -``` - -**Important Notes:** -- The expansion amount must equal the previous staking amount (increases are not yet supported) -- The finality providers used to construct the expansion staking transaction scripts must be a superset of those from the previous staking (all previous finality providers must be included, with additional ones allowed) -- The expansion transaction requires covenant signatures to spend the previous staking output -- The funding UTXO is used only to cover transaction fees, not to increase the staking amount - -#### Collecting Expansion Covenant Signatures - -Similar to unbonding transactions, staking expansion requires covenant signatures to spend the previous staking output: - -```ts -// Create the full witness with covenant signatures -const witness = createCovenantWitness( - signedExpansionTx.ins[0].witness, // original witness from signed transaction - covenantPks: Buffer[], - covenantExpansionSignatures: { - btc_pk_hex: string; - sig_hex: string; - }[], - covenantQuorum -); - -// Attach the witness to the expansion transaction -signedExpansionTx.ins[0].witness = witness; -``` - -### Create unbonding transaction - -The staking script allows users to on-demand unbond their locked stake before -the staking transaction timelock expires, subject to an unbonding period. - -The unbonding transaction can be created as follows: - -```ts -import { unbondingTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction } from "bitcoinjs-lib"; - -// Unbonding fee in satoshis. number -const unbondingFee: number = 500; - -const unsignedUnbondingPsbt: {psbt: Psbt} = unbondingTransaction( - scripts: { - unbondingTimelockScript, - slashingScript, - }, - stakingTx, - unbondingFee, - network, - outputIndex // The staking transaction output path index -); - -const signedUnbondingPsbt = await signPsbt(unsignedUnbondingPsbt.psbt.toHex()); -const unbondingTx = Psbt.fromHex(signedUnbondingPsbt).extractTransaction(); -``` - -#### Collecting Unbonding Signatures - -The unbonding transaction requires two types of signatures to be valid and -acceptable by the Bitcoin network: -1. The staker's signature -2. The covenant committee signatures - -To obtain a complete, valid unbonding transaction that can be submitted to -Bitcoin, you'll need to retrieve the covenant signatures from the Babylon -network after: -- Your delegation has been successfully registered on Babylon -- The covenant committee has verified your delegation - -You can obtain these covenant signatures either by: -- Querying the Babylon node directly -- Using the Babylon API endpoints - -Once you have both the staker's signature and the covenant signatures, -you can combine them like this: - -```ts -// Create the full witness -const witness = createCovenantWitness( - unbondingTx.ins[0].witness: Buffer[], // original witness - covenantPks: Buffer[], - covenantUnbondingSignatures: { - btc_pk_hex: string; - sig_hex: string; - }[], - covenantQuorum -); - -// Put the witness inside the unbonding transaction. -unbondingTx.ins[0].witness = witness;; -``` - -### Withdrawing - -Withdrawing involves extracting funds for which the staking/unbonding period has expired from the staking/unbonding transaction. - -Initially, we specify the withdrawal transaction parameters. - -```ts -// The index of the staking/unbonding output in the staking/unbonding -// transcation. -const stakingOutputIndex: number = 0; - -// The fee that the withdrawl transaction should use. -const withdrawalFee: number = 500; - -// The address to which the funds should be withdrawed to. -const withdrawalAddress: string = btcWallet.address; -``` - -Then, we construct the withdrawal transaction. -There are three types of withdrawal - -1. Withdraw funds from a staking transaction in which the timelock naturally expired: - -```ts -import { Psbt, Transaction } from "bitcoinjs-lib"; -import { withdrawTimelockUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; - -// staking transaction. Transaction -const stakingTx: Transaction = undefined; - -const unsignedWithdrawalPsbt: {psbt: Psbt, fee: number} = withdrawTimelockUnbondedTransaction( - scripts: { - timelockScript, - slashingScript, - unbondingScript, - }, - stakingTx, - btcWallet.address, - network, - feeRate, - stakingOutputIndex, -); -``` - -2. Withdraw funds from an unbonding transaction that was submitted for early unbonding and the unbonding period has passed: - -```ts -import { Psbt, Transaction } from "bitcoinjs-lib"; -import { withdrawEarlyUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; - -const unsignedWithdrawalPsbt: { psbt: Psbt, fee: number } = withdrawEarlyUnbondedTransaction( - scripts: { - unbondingTimelockScript, - slashingScript, - }, - unbondingTx, - withdrawalAddress, - network, - feeRate, -); - -const signedWithdrawalPsbt = await signPsbt(unsignedWithdrawalPsbt.psbt.toHex()); -const withdrawalTransaction = Psbt.fromHex(signedWithdrawalPsbt).extractTransaction(); -``` - -3. Withdraw from a slashed transaction where its timelock has expired - -```ts -import { Psbt, Transaction } from "bitcoinjs-lib"; -import { withdrawSlashingTransaction } from "@babylonlabs-io/btc-staking-ts"; - - -const unsignedWithdrawalPsbt: { psbt: Psbt, fee: number } = withdrawSlashingTransaction( - scripts: { - unbondingTimelockScript, - }, - slashingTx, - withdrawalAddress, - network, - feeRate, - outputIndex // the output index from the slashing tx -); - -const signedWithdrawalPsbt = await signPsbt(unsignedWithdrawalPsbt.psbt.toHex()); -const withdrawalTransaction = Psbt.fromHex(signedWithdrawalPsbt).extractTransaction(); -``` - -### Create slashing transaction - -The slashing transaction is the transaction that is sent to Bitcoin in the event of the finality provider in which the stake has been delegated to performs an offence. - -First, collect the parameters related to slashing. -These are Babylon parameters and should be collected from the Babylon system. - -```ts -// The public key script to send the slashed funds to. -const slashingPkScriptHex: string = ""; -// 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; -``` - -Then create and sign the slashing transaction. -There are two types of slashing transactions: - -1. Slashing of the staking transaction when no unbonding has been performed: - -```ts -import { slashTimelockUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; -import { Psbt, Transaction } from "bitcoinjs-lib"; - -const outputIndex: number = 0; - -const unsignedSlashingPsbt: {psbt: Psbt} = slashTimelockUnbondedTransaction( - scripts: { - slashingScript, - unbondingScript, - timelockScript, - unbondingTimelockScript, - }, - stakingTx, - slashingPkScriptHex, - slashingRate, - minimumSlashingFee, - network, - outputIndex, -); - -const signedSlashingPsbt = await signPsbt(unsignedSlashingPsbt.psbt.toHex()); -const slashingTx = Psbt.fromHex(signedSlashingPsbt).extractTransaction(); -``` - -2. Slashing of the unbonding transaction in the case of on-demand unbonding: - -create unsigned unbonding slashing transaction - -```ts -import { Psbt, Transaction } from "bitcoinjs-lib"; -import { slashEarlyUnbondedTransaction } from "@babylonlabs-io/btc-staking-ts"; - -const unsignedUnbondingSlashingPsbt: {psbt: Psbt} = slashEarlyUnbondedTransaction( - scripts: { - slashingScript, - unbondingTimelockScript, - }, - unbondingTx, - slashingPkScriptHex, - slashingRate, - minimumSlashingFee, - network, -); - -const signedUnbondingSlashingPsbt = await signPsbt(unsignedUnbondingSlashingPsbt.psbt.toHex()); -const unbondingSlashingTx = Psbt.fromHex(signedUnbondingSlashingPsbt).extractTransaction(); -``` diff --git a/modules/babylonlabs-io-btc-staking-ts/docs/usage.md b/modules/babylonlabs-io-btc-staking-ts/docs/usage.md deleted file mode 100644 index a740e9f3dd..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/docs/usage.md +++ /dev/null @@ -1,517 +0,0 @@ -# Babylon Bitcoin Staking - Usage Guide - -This document describes how to use the Bitcoin Staking -TypeScript library to generate the Bitcoin and Babylon Genesis -transactions associated with the Bitcoin staking protocol. -For a comprehensive review of the protocol for the registration -of Bitcoin stakes on the Babylon Genesis chain and the transactions involved, -please read the [stake registration -documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md). - -## Table of Contents - -- [Babylon Bitcoin Staking - Usage Guide](#babylon-bitcoin-staking---usage-guide) - - [Table of Contents](#table-of-contents) - - [1. Prerequisites](#1-prerequisites) - - [1.1 Staking Parameters](#11-staking-parameters) - - [1.2 Bitcoin Staker information](#12-bitcoin-staker-information) - - [1.3 Signing Providers](#13-signing-providers) - - [2. Staking Manager Initialization](#2-staking-manager-initialization) - - [3. Stake Registration](#3-stake-registration) - - [3.1 Post-Staking Registration](#31-post-staking-registration) - - [3.2 Pre-Staking Registration](#32-pre-staking-registration) - - [4. Delegation Expansion](#4-delegation-expansion) - - [4.1 Staking Expansion Registration](#41-staking-expansion-registration) - - [4.2 Create Signed Staking Expansion Transaction](#42-create-signed-staking-expansion-transaction) - - [5. Unbonding Transaction](#5-unbonding-transaction) - - [6. Withdrawal Transaction](#6-withdrawal-transaction) - - [7. Fee Calculation](#7-fee-calculation) - - [7.1 Bitcoin Transaction Fee](#71-bitcoin-transaction-fee) - - [7.2 Babylon Genesis Transaction Fee](#72-babylon-genesis-transaction-fee) - -## 1. Prerequisites - -### 1.1 Staking Parameters - -The Bitcoin Staking parameters define the conditions -that the Bitcoin Staking script and the Bitcoin Staking -transaction containing it must satisfy. -They are versioned parameters, with each version corresponding -to a range of Bitcoin heights. - -You can retrieve the parameters as follows: -* By querying the `/babylon/btcstaking/v1/params` endpoint - of an RPC/LCD node. You can find the available RPC/LCD nodes - of each active network in the - [Babylon networks Information](https://docs.babylonlabs.io/developers/babylon_genesis_chain/node_information/). -* By querying the `/v2/network-info` endpoint of the - [Babylon Staking API](https://docs.babylonlabs.io/api/staking-api/get-network-info/) - that exposes the indexed Babylon parameters. - -To learn more about the Bitcoin staking parameters and their usage in -constructing and validating Bitcoin Staking transactions, please refer to the -[specification](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md#32-babylon-chain-btc-staking-parameters). - -> **Important**: The parameters are subject to change as they are controlled -> by Babylon Genesis governance. Please make sure that you understand how to -> select the correct parameters before creating your Bitcoin Staking -> transaction. Usage of the incorrect parameters might lead to (temporary or not) -> fund loss. - -### 1.2 Bitcoin Staker information - -**Staker's Bitcoin Details** - -The staker's *Bitcoin address* serves two key purposes within this library: -- Acts as the change address when creating Bitcoin staking transactions -- Serves as the withdrawal address when creating Bitcoin withdrawal transactions - -The staker's *Bitcoin public key* is essential throughout the creation of all -Bitcoin transactions as it is used to construct the tapscript. - -> **Important**: The Bitcoin address should always correspond to the Bitcoin -> public key and be generated by it. - -Staker inputs define the parameters for Bitcoin staking. Stakers can customize -their: -- Staking timelock duration -- Staking amount -- Selected finality providers for delegation - -> **Note**: The other parameters included in the -> [Bitcoin Staking script](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/staking-script.md) -> are retrieved through the Bitcoin Staking parameters. - -```ts -const stakerInfo = { - // BTC Address - address: string, - // BTC compressed public Key in the 32-byte x-coordinate only hex format. - publicKeyNoCoordHex: string, -} - -const stakerInput = { - // The chosen finality providers public keys in compressed 32 bytes x-coordinate - // only format. - finalityProviderPksNoCoordHex: string[], - // Amount of satoshis staker choose to stake - stakingAmountSat: number, - // The number of BTC blocks this staking transaction will be staked for - stakingTimelock: number -} -``` - -**Staker's Babylon Genesis Details** - -The Babylon Genesis address specifies the address of the staker on the Babylon -Genesis blockchain. It is used as the signer of the -Babylon Genesis transaction to register the stake and -to create the Proof of Possession (PoP), -which must be signed during the registration process. -The PoP is used to confirm the ownership of the Bitcoin key used for staking -by the Babylon Genesis account used for stake registration. - -```ts -// Babylon bech32 address with prefix of `bbn`. -const babylonAddress = string -``` - -### 1.3 Signing Providers - -A Provider is a construct that maintains a private key that can be used for -signing operations (e.g., a wallet). - -For the purposes of this library, two providers are required: -- **Bitcoin Provider**: Responsible for signing Bitcoin transactions and -arbitrary messages through the ECDSA or BIP-322 algorithms. -- **Babylon Genesis Provider**: Responsible for signing Babylon Genesis -transactions. Babylon Genesis is based on Cosmos SDK, so providers that -support Cosmos SDK chains should be straightforward to adapt to Babylon Genesis. - -Below we define the expected interface for both of the above providers. - -```ts -export interface BtcProvider { - // Sign a PSBT - signPsbt( - psbtHex: string, - options?: SignPsbtOptions - ): Promise; - // Sign a message using the ECDSA type - // This is optional and only required if you would like to use the - // `createProofOfPossession` function - signMessage?: ( - message: string, type: "ecdsa" || "bip322-simple" - ) => Promise; -} - -export interface BabylonProvider { - // Signs a Babylon chain transaction. - // This is primarily used for signing MsgCreateBTCDelegation transactions - // which register the BTC delegation on the Babylon Genesis chain. - signTransaction: ( - msg: { - typeUrl: string; - value: T; - } - ) => Promise -} -``` - -## 2. Staking Manager Initialization - -To use the library, you'll need to create an instance -of `BabylonBtcStakingManager` with the following required parameters: - -```ts -import { BabylonBtcStakingManager } from "@babylonlabs-io/btc-staking-ts"; - -const manager = new BabylonBtcStakingManager( - btcNetwork, // Bitcoin network configuration (mainnet or testnet) - stakingParams, // Staking parameters retrieved as described in Prerequisites - btcProvider, // Bitcoin Provider for signing Bitcoin transactions - bbnProvider // Babylon Provider for signing Babylon Genesis transactions -); -``` - -The manager instance provides the necessary methods for creating and -managing Bitcoin staking transactions. Make sure you have all the prerequisites -(staking parameters and providers) properly configured before initializing the -manager. - -## 3. Stake Registration - -The Bitcoin staker utilizes the staking inputs -(stake amount, timelock, finality providers) together -with the staking parameters to construct the necessary -transactiosn required by the Bitcoin Staking protocol. - -These transactions include: -- BTC Staking Transaction: The Bitcoin transaction that locks the stake in - the self-custodial Bitcoin staking script. -- Slashing Transaction: A pre-signed transaction consenting to slashing in - case of double-signing. -- Unbonding Transaction: The on-demand unbonding transaction used to unlock - the stake before the originally committed timelock expires. -- Unbonding Slashing Transaction: A pre-signed transaction consenting to - slashing during the unbonding process in case of double-signing. - -There are two types of registrations supported by Babylon Genesis chain in which -they fits for difference purpose/use-case: - -- **Post-Staking Registration**: Should be used by stakers that already have - their Bitcoin Staking transaction included in the Bitcoin ledger (e.g., - phase-1 stakers) -- **Pre-Staking Registration**: Can be used by stakers that prefer to receive - the required stake verification guarantees before locking their funds on - Bitcoin. This is the recommended method for staking for new Babylon Genesis - stakes (i.e., ones created from phase-2 onwards). - -For more details about the two types, refer to the -[Babylon node documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md#2-bitcoin-stake-registration-methods). - -> **Important**: Phase-1 stakers should always use the post-staking -> registration method to register their phase-1 stakes. For newly created -> phase-2 stakes, both methods can be used. - - -### 3.1 Post-Staking Registration - -This flow is for stakers who already have a confirmed BTC staking transaction -(`k`-blocks deep, where `k` is defined in the staking parameters) -and want to register it on the Babylon chain. -This process is particularly suitable for Babylon Phase-1 stakes. - -The post-staking registration consists of multiple transactions and messages, -some requiring signatures from the BTC Provider: -- Bitcoin staking transaction (already on the Bitcoin network) and a proof of - inclusion -- Bitcoin unbonding transaction -- Bitcoin slashing transaction (**requires signing**) -- Bitcoin slashing unbonding transaction (**requires signing**) -- Proof of Possession (**requires signing**) - -The final constructed message will be signed by the Babylon Provider as a -Babylon Genesis transaction, ready for submission to the network. - -```ts -const { - signedBabylonTx -} = await manager.postStakeRegistrationBabylonTransaction( - stakerInfo, - stakingTx, - stakingHeight, - stakingInput, - inclusionProof, - bech32Address, -); -``` - -### 3.2 Pre-Staking Registration - -The Pre-staking registration flow is for stakers who seek verification from the -Babylon chain before submitting their BTC staking transaction to the Bitcoin -ledger. It is a multi-step process, involving: -1. The registration of the stake on the Babylon Genesis chain. -2. After verification is received, the submission of the signed Bitcoin - staking transaction to the Bitcoin ledger. -3. Once the Bitcoin staking transaction has received sufficient confirmations, - the Babylon Genesis chain is notified about its inclusion. - -The registration to the Babylon Genesis blockchain -consists of the submission of multiple transactions and messages, some requiring -signatures from the BTC Provider: -- Bitcoin staking transaction -- Bitcoin unbonding transaction -- Bitcoin slashing transaction (**requires signing**) -- Bitcoin slashing unbonding transaction (**requires signing**) -- Proof of Possession (**requires signing**) - -First, we create the initial registration message that will be signed -by the Babylon Provider as a Babylon Genesis transaction, -ready for submission to the network. - -```ts -const { - signedBabylonTx -} = await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - babylonBtcTipHeight, - inputUTXOs, - feeRate, - bech32Address, -); -``` - -Next, we monitor for the transaction receiving verification by the Babylon -blockchain. We can do so by querying the Babylon node and checking for the -`VERIFIED` status as follows: -- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` - endpoint of an RPC/LCD node. For more details, see this - [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). -- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the - Babylon Staking API. For more details, see this - [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). - -After the stake has been marked as verified by the Babylon Genesis chain, -we can construct the Bitcoin Staking transaction that is ready to be -broadcast to the Bitcoin ledger. -```ts -const signedBtcStakingTx = await manager.createSignedBtcStakingTransaction({ - stakerInfo, - stakingInput, - unsignedStakingTx, - inputUTXOs, - stakingParamsVersion -}) -``` - -Once the staking transaction has been included in the Bitcoin ledger with -sufficient confirmations, an off-chain program called the -[vigilante BTC Staking tracker](https://github.com/babylonlabs-io/vigilante) -notifies the Babylon chain about the staking transaction's inclusion. - -## 4. Delegation Expansion - -Delegation expansion allows you to extend an existing BTC stake with additional -finality providers or renew the timelock without going through the full unbonding -process. For more details, please refer to the -[BTC Stake Expansion](https://github.com/babylonlabs-io/babylon/blob/v3.0.0-rc.1/docs/bitcoin-stake-expansion.md). - -The expansion process involves: -1. Creating an unsigned staking expansion transaction -2. Registering the expansion on the Babylon Genesis chain -3. Signing and submitting the expansion transaction to Bitcoin - -### 4.1 Staking Expansion Registration - -First, create the initial expansion registration message that will be signed -by the Babylon Provider as a Babylon Genesis transaction. - -```ts -const { - signedBabylonTx, - stakingTx: stakingExpansionTx -} = await manager.stakingExpansionRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - babylonBtcTipHeight, - inputUTXOs, - feeRate, - babylonAddress, - { - stakingTx: previousStakingTx, - paramVersion: previousStakingParamsVersion, - stakingInput: previousStakingInput, - } -); -``` - -**Important Notes:** -- The expansion amount must equal the previous staking amount (increases are -not yet supported). -- All finality providers from the previous staking must be included in the -expansion. You can retrieve the previous staking information through the -`/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the Babylon Staking API. -- The input UTXOs are used only to cover transaction fees, not to increase -the staking amount (this feature is not yet supported). The method -automatically selects the optimal UTXO to cover the fees. - -### 4.2 Create Signed Staking Expansion Transaction - -After the expansion has been registered and verified by the Babylon chain, -you can create the signed staking expansion transaction. This requires -covenant signatures from the covenant committee. - -You can retrieve the covenant expansion signatures through either: -- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` - endpoint of an RPC/LCD node -- By querying the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the - Babylon Staking API - -```ts -const signedStakingExpansionTx = await manager.createSignedBtcStakingExpansionTransaction( - stakerInfo, - stakingInput, - unsignedStakingExpansionTx, - inputUTXOs, - stakingParamsVersion, - { - stakingTx: previousStakingTx, - paramVersion: previousStakingParamsVersion, - stakingInput: previousStakingInput, - }, - covenantStakingExpansionSignatures -); -``` - -The resulting transaction is ready to be submitted to the Bitcoin network. - -## 5. Unbonding Transaction - -This step allows stakers to unbond their active staking transactions on demand -before the committed timelock expires. After unbonding, the funds will become -available for withdrawal once the unbonding period -(specified in the staking parameters) has elapsed. - -The unbonding transaction requires signatures from both the staker and the -covenant committee. This step combines these signatures to create a complete -transaction ready for submission to the Bitcoin network. - -You can retrieve the unsigned unbonding transaction and covenant committee -signatures through either: -- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` - endpoint of an RPC/LCD node. For more details, see this - [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). -- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the - Babylon Staking API. For more details, see this - [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). - -```ts -const { signedUnbondingTx } = await manager.createSignedBtcUnbondingTransaction({ - stakerInfo, - stakingInput, - stakingParamsVersion, - stakingTx, - unsignedUnbondingTx, - covenantUnbondingSignatures -}) -``` - -## 6. Withdrawal Transaction - -There are 3 different types of withdrawal transactions: -1. Withdraw from early unbonding (`createSignedBtcWithdrawEarlyUnbondedTransaction`) - - Used when the unbonding period has passed -2. Withdraw from expired timelock (`createSignedBtcWithdrawStakingExpiredTransaction`) - - Used when the staking period has naturally ended -3. Withdraw from slashed stake (`createSignedBtcWithdrawSlashingTransaction`) - - Used when withdrawing slashed funds after timelock expiry - -All withdrawal transactions will direct the change balance to the staker's -address (provided via `stakerInfo`). -For more customized transaction options, -please refer to the [advanced usage documentation](docs/advanced-btc-tx.md). - -The required input transaction varies depending on the withdrawal method: -- Early unbonding withdrawal requires the unbonding transaction -- Timelock expiry withdrawal requires the staking transaction -- Slashed stake withdrawal requires the slashing transaction - -You can retrieve the Bitcoin staking transaction, unsigned unbonding transaction -, slashing transaction and staking input made at the time of creating the staking -transaction through either: -- By querying the `/babylon/btcstaking/v1/btc_delegation/:staking_tx_hash_hex` - endpoint of an RPC/LCD node. For more details, see this - [Babylon API documentation](https://docs.babylonlabs.io/api/babylon-gRPC/btc-delegation/). -- By queryign the `/v2/delegation?staking_tx_hash_hex=xxx` endpoint from the - Babylon Staking API. For more details, see this - [Staking API documentation](https://docs.babylonlabs.io/api/staking-api/get-a-delegation/). - - -```ts -// 1. Withdraw from early unbonding (when unbonding period has passed) -const signedWithdrawEarlyUnbondedTx = await manager.createSignedBtcWithdrawEarlyUnbondedTransaction({ - stakerInfo, - stakingInput, - stakingParamsVersion, - unbondingTx, // Withdraw from unbonding transaction - feeRate -}) - -// 2. Withdraw from expired timelock (when staking period has naturally ended) -const signedWithdrawTimelockExpiredTx = await manager.createSignedBtcWithdrawStakingExpiredTransaction({ - stakerInfo, - stakingInput, - stakingParamsVersion, - stakingTx, // Withdraw from staking transaction - feeRate -}) - -// 3. Withdraw from slashed Bitcoin Staking transaction (after timelock expiry) -const signedWithdrawSlashedTx = await manager.createSignedBtcWithdrawSlashingTransaction({ - stakerInfo, - stakingInput, - stakingParamsVersion, - slashingTx, // Withdraw from slashing transaction - feeRate -}) -``` - -## 7. Fee Calculation - -### 7.1 Bitcoin Transaction Fee - -The library's fee calculation for Bitcoin transactions is based on an estimated -size of the transaction in virtual bytes (vB). This estimation helps in -calculating the appropriate fee to include in the transaction to ensure it is -processed by the Bitcoin network efficiently. - -> **Note**: The fee estimation is only used for transactions in which the -> protocol allows to specify a custom fee, i.e., the staking and withdrawal -> transactions. The slashing and unbonding transactions have a pre-defined fee -> amount that should be used based on the Bitcoin Staking parameters utilized -> for the staking operation. Please refer to the -> [staking registration documentation](https://github.com/babylonlabs-io/babylon/blob/release/v1.x/docs/register-bitcoin-stake.md) -> for more details. - -```ts -// Calculate the estimated fee for a staking transaction -const feeSats = manager.estimateBtcStakingFee({ - stakerInfo, - babylonBtcTipHeight, - stakingInput, - inputUTXOs, - feeRate -}) -``` - -### 7.2 Babylon Genesis Transaction Fee - -The current version of the library does support functionality for calculating -the Babylon Genesis transaction fees for the `pre-staking registration` -and `post-staking registration` operations. This feature will be added in a -future release. -For now please refer to the -[simple-staking example](https://github.com/babylonlabs-io/simple-staking/blob/main/src/app/hooks/client/rpc/mutation/useBbnTransaction.ts#L27). diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts deleted file mode 100644 index 2df6136235..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/base.ts +++ /dev/null @@ -1,488 +0,0 @@ -import * as ecc from "@bitcoin-js/tiny-secp256k1-asmjs"; -import * as bitcoin from "bitcoinjs-lib"; -import ECPairFactory from "ecpair"; -import { - slashEarlyUnbondedTransaction, - slashTimelockUnbondedTransaction, - unbondingTransaction, -} from "../../../src"; -import { Staking } from "../../../src/staking"; -import { UTXO } from "../../../src/types/UTXO"; -import { StakingParams } from "../../../src/types/params"; -import { generateRandomAmountSlices } from "../math"; -import { StakingScriptData, StakingScripts } from "../../../src/index"; -import { MIN_UNBONDING_OUTPUT_VALUE } from "../../../src/constants/unbonding"; -import { payments, Psbt, Transaction } from "bitcoinjs-lib"; -import { TRANSACTION_VERSION } from "../../../src/constants/psbt"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { internalPubkey } from "../../../src/constants/internalPubkey"; - -bitcoin.initEccLib(ecc); -const ECPair = ECPairFactory(ecc); - -export const DEFAULT_TEST_FEE_RATE = 15; - -export interface KeyPair { - privateKey: string; - publicKey: string; - publicKeyNoCoord: string; - keyPair: bitcoin.Signer; -} - -export type SlashingType = "earlyUnbonded" | "timelockExpire"; - -export class StakingDataGenerator { - network: bitcoin.networks.Network; - - constructor(network: bitcoin.networks.Network) { - this.network = network; - } - - generateStakingParams( - fixedTerm: boolean = false, committeeSize?: number, - minStakingAmount?: number - ): StakingParams { - if (!committeeSize) { - committeeSize = this.getRandomIntegerBetween(5, 50); - } - const covenantNoCoordPks = this.generateRandomCovenantCommittee(committeeSize).map( - (buffer) => buffer.toString("hex"), - ); - const covenantQuorum = Math.floor(committeeSize/2) + 1; - if (minStakingAmount && minStakingAmount < MIN_UNBONDING_OUTPUT_VALUE + 1) { - throw new Error("Minimum staking amount is less than the unbonding output value"); - } - const minStakingAmountSat = minStakingAmount ? minStakingAmount : this.getRandomIntegerBetween(100000, 1000000000); - const minStakingTimeBlocks = this.getRandomIntegerBetween(1, 2000); - 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, - unbondingTime, - unbondingFeeSat: minStakingAmountSat - MIN_UNBONDING_OUTPUT_VALUE - 1, - minStakingAmountSat, - maxStakingAmountSat: this.getRandomIntegerBetween( - minStakingAmountSat, minStakingAmountSat + 1000000000, - ), - minStakingTimeBlocks, - maxStakingTimeBlocks, - slashing: { - slashingRate, - slashingPkScriptHex: getRandomPaymentScriptHex(this.generateRandomKeyPair().publicKey), - minSlashingTxFeeSat, - } - }; - } - - generateMockStakingScripts( - stakerKeyPair?: KeyPair, - ): StakingScripts { - if (!stakerKeyPair) { - stakerKeyPair = this.generateRandomKeyPair(); - } - const committeeSize = this.getRandomIntegerBetween(1, 10); - const globalParams = this.generateStakingParams( - false, - committeeSize, - ); - const stakingTxTimelock = this.generateRandomTimelock(globalParams); - - return this.generateStakingScriptData( - stakerKeyPair.publicKeyNoCoord, - globalParams, - stakingTxTimelock, - ); - } - - generateStakingScriptData ( - stakerPkNoCoord: string, - params: StakingParams, - timelock: number, - ): StakingScripts { - const fpPkHex = this.generateRandomKeyPair().publicKeyNoCoord; - return new StakingScriptData( - Buffer.from(stakerPkNoCoord, "hex"), - [Buffer.from(fpPkHex, "hex")], - params.covenantNoCoordPks.map((pk: string) => Buffer.from(pk, "hex")), - params.covenantQuorum, - timelock, - params.unbondingTime, - ).buildScripts(); - } - - generateRandomTxId = () => { - const randomBuffer = Buffer.alloc(32); - for (let i = 0; i < 32; i++) { - randomBuffer[i] = Math.floor(Math.random() * 256); - } - return randomBuffer.toString("hex"); - }; - - generateRandomKeyPair = () => { - const keyPair = ECPair.makeRandom({ network: this.network }); - const { privateKey, publicKey } = keyPair; - if (!privateKey || !publicKey) { - throw new Error("Failed to generate random key pair"); - } - const pk = publicKey.toString("hex"); - - return { - privateKey: privateKey.toString("hex"), - publicKey: pk, - publicKeyNoCoord: pk.slice(2), - keyPair, - }; - }; - - // Generate a random timelock value - // ranged from 1 to 65535 - generateRandomTimelock = ( - params: { minStakingTimeBlocks: number, maxStakingTimeBlocks: number}, - ) => { - if (params.minStakingTimeBlocks === params.maxStakingTimeBlocks) { - return params.minStakingTimeBlocks; - } - return this.getRandomIntegerBetween( - params.minStakingTimeBlocks, - params.maxStakingTimeBlocks, - ); - }; - - generateRandomUnbondingTime = (timelock: number) => { - return Math.floor(Math.random() * timelock) + 1; - }; - - generateRandomFeeRates = () => { - return Math.floor(Math.random() * 1000) + 1; - }; - - // Real values will likely be in range 0.0001 to 0.3 - generateRandomSlashingRate(min: number = 0.0001, max: number = 0.3): number { - return parseFloat((Math.random() * (max - min) + min).toFixed(4)); - } - - // Convenant committee are a list of public keys that are used to sign a covenant - generateRandomCovenantCommittee = (size: number): Buffer[] => { - const committe: Buffer[] = []; - for (let i = 0; i < size; i++) { - const publicKeyNoCoord = this.generateRandomKeyPair().publicKeyNoCoord; - committe.push(Buffer.from(publicKeyNoCoord, "hex")); - } - return committe; - }; - - - getAddressAndScriptPubKey = (publicKey: string) => { - return { - taproot: this.getTaprootAddress(publicKey), - nativeSegwit: this.getNativeSegwitAddress(publicKey), - }; - }; - - getNetwork = () => { - return this.network; - }; - - generateRandomUTXOs = ( - balance: number, - numberOfUTXOs: number, - scriptPubKey?: string, - ): UTXO[] => { - if (!scriptPubKey) { - const pk = this.generateRandomKeyPair().publicKey; - const { nativeSegwit } = this.getAddressAndScriptPubKey(pk); - scriptPubKey = nativeSegwit.scriptPubKey; - } - const slices = generateRandomAmountSlices(balance, numberOfUTXOs); - return slices.map((v) => { - return { - txid: this.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: scriptPubKey, - value: v, - }; - }); - }; - - /** - * Generates a random integer between min and max. - * - * @param {number} min - The minimum number. - * @param {number} max - The maximum number. - * @returns {number} - A random integer between min and max. - */ - getRandomIntegerBetween = (min: number, max: number): number => { - if (min > max) { - throw new Error( - "The minimum number should be less than or equal to the maximum number.", - ); - } - return Math.floor(Math.random() * (max - min + 1)) + min; - }; - - /** - * The main entry point for generating a random staking transaction and - * its instance, as well as getting the staker info, params, and staking amount - * etc - * @param network - The network to use - * @param feeRate - The fee rate to use - * @param stakerKeyPair - The staker key pair to use - * @param stakingAmount - The staking amount to use - * @param addressType - The address type to use - * @param params - The staking parameters to use - * @returns {Object} - A random staking transaction - */ - generateRandomStakingTransaction = ( - network: bitcoin.networks.Network, - feeRate: number = DEFAULT_TEST_FEE_RATE, - stakerKeyPair?: KeyPair, - stakingAmount?: number, - addressType?: "taproot" | "nativeSegwit", - params?: StakingParams, - ) => { - if (!stakerKeyPair) { - stakerKeyPair = this.generateRandomKeyPair(); - } - const stakerInfo = { - address: this.getAddressAndScriptPubKey(stakerKeyPair.publicKey).nativeSegwit.address, - publicKeyNoCoordHex: stakerKeyPair.publicKeyNoCoord, - publicKeyWithCoord: stakerKeyPair.publicKey, - } - params = params ? params : this.generateStakingParams(); - const timelock = this.generateRandomTimelock(params); - const finalityProviderPksNoCoordHex = this.generateRandomFidelityProviderPksNoCoordHex(); - - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - - const stakingAmountSat = stakingAmount ? - stakingAmount : this.getRandomIntegerBetween( - params.minStakingAmountSat, params.maxStakingAmountSat, - ); - - const { publicKey } = stakerKeyPair; - const { taproot, nativeSegwit } = this.getAddressAndScriptPubKey(publicKey); - const scriptPubKey = - addressType === "taproot" - ? taproot.scriptPubKey - : nativeSegwit.scriptPubKey; - - const utxos = this.generateRandomUTXOs( - this.getRandomIntegerBetween(stakingAmountSat, stakingAmountSat + 100000000), - this.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - - const { transaction: stakingTx, fee: stakingTxFee } = staking.createStakingTransaction( - stakingAmountSat, - utxos, - feeRate, - ); - - return { - stakingTx, - timelock, - stakingInstance: staking, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - stakingAmountSat, - keyPair: stakerKeyPair, - stakingTxFee, - utxos, - scriptPubKey, - } - }; - - /** - * Generates a random slashing transaction based on the staking transaction - * and staking scripts - * @param network - The network to use - * @param stakingScripts - The staking scripts to use - * @param stakingTx - The staking transaction to use - * @param params - The params used in the staking transaction - * @param keyPair - The key pair to use. This is used to sign the slashing - * psbt to derive the transaction. - * @param type - The type of slashing to use. - * @returns {Object} - A random slashing transaction - */ - generateSlashingTransaction = ( - network: bitcoin.networks.Network, - stakingScripts: StakingScripts, - stakingTx: Transaction, - params: { - minSlashingTxFeeSat: number, - slashingPkScriptHex: string, - slashingRate: number, - }, - keyPair: KeyPair, - type: SlashingType = "timelockExpire", - ) => { - let slashingPsbt: Psbt; - let outputValue: number; - - if (type === "earlyUnbonded") { - const { transaction: unbondingTx } = unbondingTransaction( - stakingScripts, - stakingTx, - 1, - network, - ); - const { psbt } = slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - params.slashingPkScriptHex, - params.slashingRate, - params.minSlashingTxFeeSat, - network, - ); - slashingPsbt = psbt; - outputValue = unbondingTx.outs[0].value; - } else { - const { psbt } = slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - params.slashingPkScriptHex, - params.slashingRate, - params.minSlashingTxFeeSat, - network, - ); - slashingPsbt = psbt; - outputValue = stakingTx.outs[0].value; - } - - expect(slashingPsbt).toBeDefined(); - expect(slashingPsbt.txOutputs.length).toBe(2); - // first output shall send slashed amount to the slashing pk script (i.e burn output) - expect(Buffer.from(slashingPsbt.txOutputs[0].script).toString("hex")).toBe( - params.slashingPkScriptHex, - ); - expect(slashingPsbt.txOutputs[0].value).toBe( - Math.round(outputValue * params.slashingRate), - ); - - // second output is the change output which send to unbonding timelock script address - const changeOutput = payments.p2tr({ - internalPubkey, - scriptTree: { output: stakingScripts.unbondingTimelockScript }, - network, - }); - expect(slashingPsbt.txOutputs[1].address).toBe(changeOutput.address); - const expectedChangeOutputValue = - outputValue - - Math.round(outputValue * params.slashingRate) - - params.minSlashingTxFeeSat; - expect(slashingPsbt.txOutputs[1].value).toBe(expectedChangeOutputValue); - - expect(slashingPsbt.version).toBe(TRANSACTION_VERSION); - expect(slashingPsbt.locktime).toBe(0); - slashingPsbt.txInputs.forEach((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - - const tx = slashingPsbt.signAllInputs( - keyPair.keyPair, - ).finalizeAllInputs().extractTransaction(); - - return { - psbt: slashingPsbt, - tx, - }; - } - - randomBoolean(): boolean { - return Math.random() >= 0.5; - }; - - generateRandomScriptPubKey = ({isTaproot}: {isTaproot?: boolean} = {}): string => { - const pk = this.generateRandomKeyPair().publicKey; - const { taproot, nativeSegwit } = this.getAddressAndScriptPubKey(pk); - if (isTaproot) { - return taproot.scriptPubKey; - } - return nativeSegwit.scriptPubKey; - }; - - private getTaprootAddress = (publicKeyWithCoord: string) => { - // Remove the prefix if it exists - let publicKeyNoCoord = ""; - if (publicKeyWithCoord.length == 66) { - publicKeyNoCoord = publicKeyWithCoord.slice(2); - } - const internalPubkey = Buffer.from(publicKeyNoCoord, "hex"); - const { address, output: scriptPubKey } = bitcoin.payments.p2tr({ - internalPubkey, - network: this.network, - }); - if (!address || !scriptPubKey) { - throw new Error( - "Failed to generate taproot address or script from public key", - ); - } - return { - address, - scriptPubKey: scriptPubKey.toString("hex"), - }; - }; - - private getNativeSegwitAddress = (publicKey: string) => { - // check the public key length is 66, otherwise throw - if (publicKey.length !== 66) { - throw new Error( - "Invalid public key length for generating native segwit address", - ); - } - const internalPubkey = Buffer.from(publicKey, "hex"); - const { address, output: scriptPubKey } = bitcoin.payments.p2wpkh({ - pubkey: internalPubkey, - network: this.network, - }); - if (!address || !scriptPubKey) { - throw new Error( - "Failed to generate native segwit address or script from public key", - ); - } - return { - address, - scriptPubKey: scriptPubKey.toString("hex"), - }; - }; - - generateRandomFidelityProviderPksNoCoordHex = ( - numberOfFidelityProviders: number = 10, - ) => { - const finalityProviderPksNoCoordHex: string[] = []; - for (let i = 0; i < numberOfFidelityProviders; i++) { - finalityProviderPksNoCoordHex.push(this.generateRandomKeyPair().publicKeyNoCoord); - } - return finalityProviderPksNoCoordHex; - } -} - -export const 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"); -} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts deleted file mode 100644 index 5ec738c6bc..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/helper/datagen/observable.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { ObservableStakingScriptData, ObservableStakingScripts } from "../../../src/staking/observable"; -import { ObservableVersionedStakingParams } from "../../../src/types/params"; -import { StakingDataGenerator } from "./base"; - -export class ObservableStakingDatagen extends StakingDataGenerator { - generateRandomTag = () => { - const buffer = Buffer.alloc(4); - for (let i = 0; i < 4; i++) { - buffer[i] = Math.floor(Math.random() * 256); - } - return buffer; - }; - - generateStakingParams = ( - fixedTerm = false, - committeeSize?: number, - minStakingAmount?: number, - ): ObservableVersionedStakingParams => { - return { - ...super.generateStakingParams(fixedTerm, committeeSize, minStakingAmount), - btcActivationHeight: this.getRandomIntegerBetween(1000, 100000), - tag: this.generateRandomTag().toString("hex"), - version: this.getRandomIntegerBetween(1, 10), - }; - }; - - generateStakingScriptData = ( - stakerPkNoCoord: string, - params: ObservableVersionedStakingParams, - timelock: number, - ): ObservableStakingScripts => { - const fpPkHex = this.generateRandomKeyPair().publicKeyNoCoord; - return new ObservableStakingScriptData( - Buffer.from(stakerPkNoCoord, "hex"), - [Buffer.from(fpPkHex, "hex")], - params.covenantNoCoordPks.map((pk: string) => Buffer.from(pk, "hex")), - params.covenantQuorum, - timelock, - params.unbondingTime, - Buffer.from(params.tag, "hex"), - ).buildScripts(); - } -} \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts deleted file mode 100644 index d974fddbd9..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/helper/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as testingNetworks } from "./testingNetworks"; -export const DEFAULT_TEST_FEE_RATE = 15; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts deleted file mode 100644 index 57f605542c..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/helper/math.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Generates an array of random integers for each slice that sum up to the total amount. - * - * @param totalAmount - The total amount to be distributed across the slices (must be an integer). - * @param numOfSlices - The number of slices (must be an integer). - * @returns An array of integers representing the amount for each slice. - */ -export const generateRandomAmountSlices = ( - totalAmount: number, - numOfSlices: number, -): number[] => { - if (numOfSlices <= 0) { - throw new Error("Number of slices must be greater than zero."); - } - - const amounts: number[] = []; - let remainingAmount = totalAmount; - - for (let i = 0; i < numOfSlices - 1; i++) { - const max = Math.floor(remainingAmount / (numOfSlices - i)); - const amount = Math.floor(Math.random() * max); - amounts.push(amount); - remainingAmount -= amount; - } - - // Push the remaining amount as the last slice amount - amounts.push(remainingAmount); - - return amounts; -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts b/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts deleted file mode 100644 index 5a6d9789ff..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/helper/testingNetworks.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as bitcoin from "bitcoinjs-lib"; -import { StakingDataGenerator } from "./datagen/base"; - -export interface NetworkConfig { - networkName: string; - network: bitcoin.Network; - datagen: { - stakingDatagen: StakingDataGenerator; - } -} - -const createNetworkConfig = ( - networkName: string, - network: bitcoin.Network, -): NetworkConfig => ({ - networkName, - // A deep copy of the network object to avoid referring to the same object - // in memory - network: {...network}, - datagen: { - stakingDatagen: new StakingDataGenerator(network), - }, -}); - -const testingNetworks: NetworkConfig[] = [ - createNetworkConfig("mainnet", bitcoin.networks.bitcoin), - createNetworkConfig("testnet", bitcoin.networks.testnet), -]; - -export default testingNetworks; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts deleted file mode 100644 index 6c90453c81..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createCovenantWitness.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Buffer } from "buffer"; -import { createCovenantWitness } from "../../src"; - -describe("createCovenantWitness", () => { - it("should return the correct witness when multiple covenants are matched", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - Buffer.from("covenant2", "utf-8"), - ]; - const covenantSigs = [ - // 'covenant1' and 'signature1' in hex - { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, - // 'covenant2' and 'signature2' in hex - { btcPkHex: "636f76656e616e7432", sigHex: "7369676e617475726532" }, - ]; - const covenantQuorum = 2; - - const result = createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - ); - - expect(result).toEqual([ - Buffer.from("7369676e617475726532", "hex"), // 'signature2' in hex - Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex - ...originalWitness, - ]); - }); - - it("should throw error if not enough covenant signatures", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - Buffer.from("covenant2", "utf-8"), - ]; - const covenantSigs = [ - // 'covenant1' and 'signature1' in hex - { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, - ]; - const covenantQuorum = 2; - - expect(() => createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - )).toThrow("Not enough covenant signatures. Required: 2, got: 1"); - }); - - it("should throw error if not enough valid covenant signatures after filtering", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - Buffer.from("covenant2", "utf-8"), - ]; - const covenantSigs = [ - // Valid signature for covenant1 - { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, - // Invalid signature - doesn't match any params covenant - { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, - ]; - const covenantQuorum = 2; - - expect(() => createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - )).toThrow("Not enough valid covenant signatures. Required: 2, got: 1"); - }); - - it("should throw error if all covenant signatures are invalid", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - Buffer.from("covenant2", "utf-8"), - ]; - const covenantSigs = [ - // Invalid signature - doesn't match any params covenant - { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, - // Another invalid signature - { btcPkHex: "616e6f74686572696e76", sigHex: "616e6f74686572736967" }, - ]; - const covenantQuorum = 2; - - expect(() => createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - )).toThrow("Not enough valid covenant signatures. Required: 2, got: 0"); - }); - - it("should work with mixed valid and invalid signatures when enough valid ones exist", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - Buffer.from("covenant2", "utf-8"), - ]; - const covenantSigs = [ - // Valid signature for covenant1 - { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, - // Valid signature for covenant2 - { btcPkHex: "636f76656e616e7432", sigHex: "7369676e617475726532" }, - // Invalid signature - should be ignored - { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, - ]; - const covenantQuorum = 2; - - const result = createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - ); - - expect(result).toEqual([ - Buffer.from("7369676e617475726532", "hex"), // 'signature2' in hex - Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex - ...originalWitness, - ]); - }); - - it("should work with quorum of 1 when exactly one valid signature is provided", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - ]; - const covenantSigs = [ - // Valid signature for covenant1 - { btcPkHex: "636f76656e616e7431", sigHex: "7369676e617475726531" }, - ]; - const covenantQuorum = 1; - - const result = createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - ); - - expect(result).toEqual([ - Buffer.from("7369676e617475726531", "hex"), // 'signature1' in hex - ...originalWitness, - ]); - }); - - it("should throw error when quorum is 1 but no valid signatures are provided", () => { - const originalWitness = [Buffer.from("originalWitness1", "utf-8")]; - const paramsCovenants = [ - Buffer.from("covenant1", "utf-8"), - ]; - const covenantSigs = [ - // Invalid signature - doesn't match params covenant - { btcPkHex: "696e76616c6964636f76", sigHex: "696e76616c6964736967" }, - ]; - const covenantQuorum = 1; - - expect(() => createCovenantWitness( - originalWitness, - paramsCovenants, - covenantSigs, - covenantQuorum - )).toThrow("Not enough valid covenant signatures. Required: 1, got: 0"); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts deleted file mode 100644 index dae371b62a..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createSlashingTx.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import * as stakingScript from "../../src/staking/stakingScript"; -import { testingNetworks } from "../helper"; -import * as transaction from "../../src/staking/transactions"; -import { opcodes, payments, script } from "bitcoinjs-lib"; -import { internalPubkey } from "../../src/constants/internalPubkey"; - -describe.each(testingNetworks)("Create slashing transactions", ({ - network, networkName, datagen: { stakingDatagen: dataGenerator } -}) => { - const { - stakingTx, stakingInstance, - stakerInfo, params, stakingAmountSat, - } = dataGenerator.generateRandomStakingTransaction( - network, 1 - ); - - const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction( - stakingTx, - ); - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - - describe("Create slash early unbonded transaction", () => { - it(`${networkName} should throw an error if fail to build scripts`, () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new Error("slash early unbonded delegation build script error"); - }); - - expect(() => stakingInstance.createUnbondingOutputSlashingPsbt( - unbondingTx, - )).toThrow("slash early unbonded delegation build script error"); - }); - - it(`${networkName} should throw an error if fail to build early unbonded slash tx`, () => { - jest.spyOn(transaction, "slashEarlyUnbondedTransaction").mockImplementation(() => { - throw new Error("fail to build slash tx"); - }); - expect(() => stakingInstance.createUnbondingOutputSlashingPsbt( - unbondingTx, - )).toThrow("fail to build slash tx"); - }); - - it(`${networkName} should create slash early unbonded transaction`, () => { - const slashTx = stakingInstance.createUnbondingOutputSlashingPsbt( - unbondingTx, - ); - expect(slashTx.psbt.txInputs.length).toBe(1) - expect(slashTx.psbt.txInputs[0].hash.toString("hex")). - toBe(unbondingTx.getHash().toString("hex")); - expect(slashTx.psbt.txInputs[0].index).toBe(0); - // verify outputs - expect(slashTx.psbt.txOutputs.length).toBe(2); - // slash amount - const stakingAmountLeftInUnbondingTx = unbondingTx.outs[0].value; - const slashAmount = Math.round(stakingAmountLeftInUnbondingTx * params.slashing!.slashingRate); - expect(slashTx.psbt.txOutputs[0].value).toBe( - slashAmount, - ); - expect(Buffer.from(slashTx.psbt.txOutputs[0].script).toString("hex")).toBe( - params.slashing!.slashingPkScriptHex - ); - // change output - const unbondingTimelockScript = script.compile([ - Buffer.from(stakerInfo.publicKeyNoCoordHex, "hex"), - opcodes.OP_CHECKSIGVERIFY, - script.number.encode(params.unbondingTime), - opcodes.OP_CHECKSEQUENCEVERIFY, - ]); - const { address } = payments.p2tr({ - internalPubkey, - scriptTree: { output: unbondingTimelockScript }, - network, - }); - expect(slashTx.psbt.txOutputs[1].address).toBe(address); - const userFunds = stakingAmountLeftInUnbondingTx - slashAmount - params.slashing!.minSlashingTxFeeSat; - expect(slashTx.psbt.txOutputs[1].value).toBe(userFunds); - expect(slashTx.psbt.locktime).toBe(0); - expect(slashTx.psbt.version).toBe(2); - }); - }); - - describe("Create slash timelock unbonded transaction", () => { - it(`${networkName} should throw an error if fail to build scripts`, async () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new Error("slash timelock unbonded delegation build script error"); - }); - - expect(() => stakingInstance.createStakingOutputSlashingPsbt( - stakingTx, - )).toThrow("slash timelock unbonded delegation build script error"); - }); - - it(`${networkName} should throw an error if fail to build timelock unbonded slash tx`, async () => { - jest.spyOn(transaction, "slashTimelockUnbondedTransaction").mockImplementation(() => { - throw new Error("fail to build slash tx"); - }); - - expect(() => stakingInstance.createStakingOutputSlashingPsbt( - stakingTx, - )).toThrow("fail to build slash tx"); - }); - - it(`${networkName} should create slash timelock unbonded transaction`, async () => { - const slashTx = stakingInstance.createStakingOutputSlashingPsbt( - stakingTx, - ); - expect(slashTx.psbt.txInputs.length).toBe(1) - expect(slashTx.psbt.txInputs[0].hash.toString("hex")). - toBe(stakingTx.getHash().toString("hex")); - expect(slashTx.psbt.txInputs[0].index).toBe(0); - // verify outputs - expect(slashTx.psbt.txOutputs.length).toBe(2); - // slash amount - const slashAmount = Math.round(stakingAmountSat * params.slashing!.slashingRate); - expect(slashTx.psbt.txOutputs[0].value).toBe( - slashAmount, - ); - expect(Buffer.from(slashTx.psbt.txOutputs[0].script).toString("hex")).toBe( - params.slashing!.slashingPkScriptHex - ); - // change output - const unbondingTimelockScript = script.compile([ - Buffer.from(stakerInfo.publicKeyNoCoordHex, "hex"), - opcodes.OP_CHECKSIGVERIFY, - script.number.encode(params.unbondingTime), - opcodes.OP_CHECKSEQUENCEVERIFY, - ]); - const { address } = payments.p2tr({ - internalPubkey, - scriptTree: { output: unbondingTimelockScript }, - network, - }); - expect(slashTx.psbt.txOutputs[1].address).toBe(address); - const userFunds = stakingAmountSat - slashAmount - params.slashing!.minSlashingTxFeeSat; - expect(slashTx.psbt.txOutputs[1].value).toBe(userFunds); - expect(slashTx.psbt.locktime).toBe(0); - expect(slashTx.psbt.version).toBe(2); - }); - }); -}); - diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts deleted file mode 100644 index 118b5b2eab..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createStakingTx.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { address } from "bitcoinjs-lib"; -import * as stakingScript from "../../src/staking/stakingScript"; -import { testingNetworks } from "../helper"; -import { StakingParams } from "../../src/types/params"; -import { UTXO } from "../../src/types/UTXO"; -import { StakingError, StakingErrorCode } from "../../src/error"; -import { BTC_DUST_SAT } from "../../src/constants/dustSat"; -import { NON_RBF_SEQUENCE } from "../../src/constants/psbt"; -import * as stakingUtils from "../../src/utils/staking/validation"; -import * as stakingTx from "../../src/staking/transactions"; -import { transactionIdToHash } from "../../src"; -import { Staking } from "../../src/staking"; - -describe.each(testingNetworks)("Create staking transaction", ({ - network, networkName, datagen: { stakingDatagen: dataGenerator } -}) => { - let stakerInfo: { address: string, publicKeyNoCoordHex: string, publicKeyWithCoord: string }; - let params: StakingParams; - let timelock: number; - let utxos: UTXO[]; - let finalityProviderPksNoCoordHex: string[] = []; - const feeRate = 1; - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - - const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); - const { address, scriptPubKey } = dataGenerator.getAddressAndScriptPubKey( - publicKey, - ).taproot; - - stakerInfo = { - address, - publicKeyNoCoordHex: publicKeyNoCoord, - publicKeyWithCoord: publicKey, - }; - // random number of FPs - for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { - finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); - } - params = dataGenerator.generateStakingParams(true); - timelock = dataGenerator.generateRandomTimelock(params); - utxos = dataGenerator.generateRandomUTXOs( - params.maxStakingAmountSat * dataGenerator.getRandomIntegerBetween(1, 100), - dataGenerator.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - }); - - it(`${networkName} throw StakingError if stakerInfo is incorrect`, async () => { - const stakerInfoWithCoordPk = { - address: stakerInfo.address, - publicKeyNoCoordHex: stakerInfo.publicKeyWithCoord, - }; - expect(() => new Staking( - network, stakerInfoWithCoordPk, - params, finalityProviderPksNoCoordHex, timelock, - )).toThrow( - "Invalid staker public key" - ); - - const stakerInfoWithInvalidAddress = { - address: "abc", - publicKeyNoCoordHex: stakerInfo.publicKeyNoCoordHex, - }; - expect(() => new Staking( - network, stakerInfoWithInvalidAddress, - params, finalityProviderPksNoCoordHex, timelock, - )).toThrow( - "Invalid staker bitcoin address" - ); - }); - - it(`${networkName} should throw an error if input data validation failed`, async () => { - jest.spyOn(stakingUtils, "validateStakingTxInputData").mockImplementation(() => { - throw new StakingError(StakingErrorCode.INVALID_INPUT, "some error"); - }); - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - - expect(() => staking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.INVALID_INPUT, "some error") - ); - }); - - it(`${networkName} should throw an error if fail to build scripts`, async () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error"); - }); - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - - expect(() => staking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error") - ); - }); - - it(`${networkName} should throw an error if fail to build staking tx`, async () => { - jest.spyOn(stakingTx, "stakingTransaction").mockImplementation(() => { - throw new Error("fail to build staking tx"); - }); - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - - expect(() => staking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.BUILD_TRANSACTION_FAILURE, "fail to build staking tx") - ); - }); - - it(`${networkName} should throw an error if fail to validate staking output`, async () => { - // Setup - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - const amount = dataGenerator.getRandomIntegerBetween( - params.minStakingAmountSat, params.maxStakingAmountSat, - ); - - // Create transaction and psbt - const { transaction } = staking.createStakingTransaction( - amount, - utxos, - feeRate, - ); - - // Setup a different param - const wrongParams = dataGenerator.generateStakingParams(); - const wrongTimelock = dataGenerator.generateRandomTimelock(wrongParams); - const wrongStaking = new Staking( - network, stakerInfo, - wrongParams, finalityProviderPksNoCoordHex, wrongTimelock, - ); - - expect(() => wrongStaking.toStakingPsbt(transaction, utxos)).toThrow( - expect.objectContaining({ - code: StakingErrorCode.INVALID_OUTPUT, - message: expect.stringContaining("Matching output not found") - }) - ); - }); - - it(`${networkName} should successfully create a staking transaction & psbt`, async () => { - // Setup - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - const amount = dataGenerator.getRandomIntegerBetween( - params.minStakingAmountSat, params.maxStakingAmountSat, - ); - - // Create transaction and psbt - const { transaction, fee } = staking.createStakingTransaction( - amount, - utxos, - feeRate, - ); - const psbt = staking.toStakingPsbt(transaction, utxos); - - // Basic validation - expect(transaction).toBeDefined(); - expect(fee).toBeGreaterThan(0); - expect(transaction.version).toBe(2); - expect(psbt.version).toBe(2); - - // Validate inputs - expect(transaction.ins.length).toBeGreaterThan(0); - expect(psbt.data.inputs.length).toBe(transaction.ins.length); - expect(psbt.data.inputs[0].tapInternalKey?.toString("hex")).toEqual(stakerInfo.publicKeyNoCoordHex); - expect(psbt.data.inputs[0].witnessUtxo?.script.toString("hex")).toEqual(utxos[0].scriptPubKey); - - // Validate sequences - transaction.ins.forEach(input => expect(input.sequence).toBe(NON_RBF_SEQUENCE)); - psbt.txInputs.forEach(input => expect(input.sequence).toBe(NON_RBF_SEQUENCE)); - - // Calculate and validate amounts - const psbtInputAmount = psbt.data.inputs.reduce((sum, input) => - sum + (input.witnessUtxo?.value || 0), 0); - const txInputAmount = transaction.ins.reduce((sum, input) => { - const matchingUtxo = utxos.find(utxo => - transactionIdToHash(utxo.txid).toString("hex") === input.hash.toString("hex") - && utxo.vout === input.index); - return sum + (matchingUtxo?.value || 0); - }, 0); - - expect(psbtInputAmount).toBeGreaterThanOrEqual(amount + fee); - expect(txInputAmount).toBeGreaterThanOrEqual(amount + fee); - - // Validate change outputs if present - const psbtChangeAmount = psbtInputAmount - amount - fee; - const txChangeAmount = txInputAmount - amount - fee; - expect(psbtChangeAmount).toEqual(txChangeAmount); - - if (psbtChangeAmount > BTC_DUST_SAT) { - const lastPsbtOutput = psbt.txOutputs[psbt.txOutputs.length - 1]; - const lastTxOutput = transaction.outs[transaction.outs.length - 1]; - - expect(lastPsbtOutput.value).toEqual(psbtChangeAmount); - expect(lastPsbtOutput.address).toEqual(stakerInfo.address); - expect(lastTxOutput.value).toEqual(txChangeAmount); - expect(lastTxOutput.script).toEqual(address.toOutputScript(stakerInfo.address, network)); - } - - // Validate staking amount output - expect(psbt.txOutputs[0].value).toEqual(amount); - expect(transaction.outs[0].value).toEqual(amount); - - // Validate transaction and psbt match - expect(psbt.locktime).toEqual(transaction.locktime); - expect(psbt.txOutputs.length).toEqual(transaction.outs.length); - - // Validate all inputs match between psbt and transaction - psbt.txInputs.forEach((input, i) => { - const txInput = transaction.ins[i]; - expect(input.hash).toEqual(txInput.hash); - expect(input.index).toEqual(txInput.index); - expect(input.sequence).toEqual(txInput.sequence); - }); - - // Validate all outputs match between psbt and transaction - psbt.txOutputs.forEach((output, i) => { - const txOutput = transaction.outs[i]; - expect(output.value).toEqual(txOutput.value); - expect(output.script).toEqual(txOutput.script); - }); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts deleted file mode 100644 index 6056758d8f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createUnbondingtx.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Staking } from "../../src/staking"; -import * as transaction from "../../src/staking/transactions"; -import { internalPubkey } from "../../src/constants/internalPubkey"; -import { StakingError, StakingErrorCode } from "../../src/error"; -import { testingNetworks } from "../helper"; -import { NON_RBF_SEQUENCE } from "../../src/constants/psbt"; -import * as stakingScript from "../../src/staking/stakingScript"; -import { deriveStakingOutputInfo, findMatchingTxOutputIndex } from "../../src/utils/staking"; - -describe.each(testingNetworks)("Create unbonding transaction", ({ - network, networkName, datagen: { stakingDatagen : dataGenerator } -}) => { - const feeRate = 1; - const { - stakingTx, - timelock, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - stakingAmountSat, - } = dataGenerator.generateRandomStakingTransaction( - network, - feeRate, - ); - let staking: Staking; - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - }); - - it(`${networkName} should throw an error if fail to build scripts`, async () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "build script error"); - }); - - expect(() => staking.createUnbondingTransaction( - stakingTx, - )).toThrow("build script error"); - }); - - it(`${networkName} should throw an error if fail to build unbonding tx`, async () => { - jest.spyOn(transaction, "unbondingTransaction").mockImplementation(() => { - throw new Error("fail to build unbonding tx"); - }); - expect(() => staking.createUnbondingTransaction( - stakingTx, - )).toThrow("fail to build unbonding tx"); - }); - - it(`${networkName} should successfully create an unbonding transaction & psbt`, async () => { - // Create transaction and psbt - const { transaction } = staking.createUnbondingTransaction(stakingTx); - const scripts = staking.buildScripts(); - const psbt = staking.toUnbondingPsbt(transaction, stakingTx); - - // Basic validation - expect(transaction.version).toBe(2); - expect(psbt.version).toBe(2); - expect(transaction.locktime).toBe(0); - expect(psbt.locktime).toBe(0); - - // Get staking output index - const stakingOutputIndex = findMatchingTxOutputIndex( - stakingTx, - deriveStakingOutputInfo(scripts, network).outputAddress, - network, - ); - - // Validate inputs - expect(transaction.ins.length).toBe(1); - expect(psbt.data.inputs.length).toBe(1); - expect(transaction.ins[0].hash).toEqual(stakingTx.getHash()); - expect(psbt.txInputs[0].hash).toEqual(stakingTx.getHash()); - expect(transaction.ins[0].index).toEqual(stakingOutputIndex); - expect(psbt.txInputs[0].index).toEqual(stakingOutputIndex); - expect(transaction.ins[0].sequence).toEqual(NON_RBF_SEQUENCE); - expect(psbt.txInputs[0].sequence).toEqual(NON_RBF_SEQUENCE); - - // Validate PSBT input details - expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); - expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); - expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(stakingAmountSat); - expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( - stakingTx.outs[stakingOutputIndex].script - ); - - // Validate outputs - expect(transaction.outs.length).toBe(1); - expect(psbt.txOutputs.length).toBe(1); - expect(transaction.outs[0].value).toEqual(stakingAmountSat - params.unbondingFeeSat); - expect(psbt.txOutputs[0].value).toEqual(stakingAmountSat - params.unbondingFeeSat); - - // Validate transaction and psbt match - expect(psbt.txOutputs[0].script).toEqual(transaction.outs[0].script); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts deleted file mode 100644 index 6c962b2414..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/createWithdrawTx.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import * as stakingScript from "../../src/staking/stakingScript"; -import { testingNetworks } from "../helper"; -import * as transaction from "../../src/staking/transactions"; -import { getWithdrawTxFee } from "../../src/utils/fee"; - -describe.each(testingNetworks)("Create withdrawal transactions", ({ - network, networkName, datagen: { stakingDatagen: dataGenerator } -}) => { - const feeRate = 1; - const { - stakingTx, - stakerInfo, - stakingInstance, - } = dataGenerator.generateRandomStakingTransaction( - network, feeRate, - ); - - const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction( - stakingTx, - ); - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - }); - - describe("Create withdraw early unbonded transaction", () => { - it(`${networkName} should throw an error if fail to build scripts`, () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new Error("withdraw early unbonded delegation build script error"); - }); - - expect(() => stakingInstance.createWithdrawEarlyUnbondedTransaction( - unbondingTx, - feeRate, - )).toThrow("withdraw early unbonded delegation build script error"); - }); - - it(`${networkName} should throw an error if fail to build early unbonded withdraw tx`, () => { - jest.spyOn(transaction, "withdrawEarlyUnbondedTransaction").mockImplementation(() => { - throw new Error("fail to build withdraw tx"); - }); - expect(() => stakingInstance.createWithdrawEarlyUnbondedTransaction( - unbondingTx, - feeRate, - )).toThrow("fail to build withdraw tx"); - }); - - it(`${networkName} should create withdraw early unbonded transaction`, () => { - const withdrawTx = stakingInstance.createWithdrawEarlyUnbondedTransaction( - unbondingTx, - feeRate, - ); - expect(withdrawTx.psbt.txInputs.length).toBe(1) - expect(withdrawTx.psbt.txInputs[0].hash.toString("hex")). - toBe(unbondingTx.getHash().toString("hex")); - expect(withdrawTx.psbt.txInputs[0].index).toBe(0); - expect(withdrawTx.psbt.txOutputs.length).toBe(1); - const fee = getWithdrawTxFee(feeRate); - expect(withdrawTx.psbt.txOutputs[0].value).toBe( - unbondingTx.outs[0].value - fee, - ); - expect(withdrawTx.psbt.txOutputs[0].address).toBe(stakerInfo.address); - expect(withdrawTx.psbt.locktime).toBe(0); - expect(withdrawTx.psbt.version).toBe(2); - }); - }); - - describe("Create timelock unbonded transaction", () => { - it(`${networkName} should throw an error if fail to build scripts`, async () => { - jest.spyOn(stakingScript, "StakingScriptData").mockImplementation(() => { - throw new Error("withdraw timelock unbonded delegation build script error"); - }); - expect(() => stakingInstance.createWithdrawStakingExpiredPsbt( - stakingTx, - feeRate, - )).toThrow("withdraw timelock unbonded delegation build script error"); - }); - - it(`${networkName} should throw an error if fail to build timelock unbonded withdraw tx`, async () => { - jest.spyOn(transaction, "withdrawTimelockUnbondedTransaction").mockImplementation(() => { - throw new Error("fail to build withdraw tx"); - }); - - expect(() => stakingInstance.createWithdrawStakingExpiredPsbt( - stakingTx, - feeRate, - )).toThrow("fail to build withdraw tx"); - }); - - it(`${networkName} should create withdraw timelock unbonded transaction`, async () => { - const withdrawTx = stakingInstance.createWithdrawStakingExpiredPsbt( - stakingTx, - feeRate, - ); - expect(withdrawTx.psbt.txInputs.length).toBe(1) - expect(withdrawTx.psbt.txInputs[0].hash.toString("hex")). - toBe(stakingTx.getHash().toString("hex")); - expect(withdrawTx.psbt.txInputs[0].index).toBe(0); - expect(withdrawTx.psbt.txOutputs.length).toBe(1); - const fee = getWithdrawTxFee(feeRate); - expect(withdrawTx.psbt.txOutputs[0].value).toBe( - stakingTx.outs[0].value - fee, - ); - expect(withdrawTx.psbt.txOutputs[0].address).toBe(stakerInfo.address); - expect(withdrawTx.psbt.locktime).toBe(0); - expect(withdrawTx.psbt.version).toBe(2); - }); - }); -}); - diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts deleted file mode 100644 index 7705d91275..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/fee.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { - getPublicKeyNoCoord, - VersionedStakingParams, - type UTXO, -} from "../../../../src"; - -export const stakerInfo = { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", - ), - address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", -}; - -export const stakerInfoArr = [ - // Taproot - stakerInfo, - // Native SegWit - { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "03d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87", - ), - address: "tb1qlphktyz6sse3meq36pjwjrsqktny4553paydg2", - }, - // Legacy - { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "028333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37", - ), - address: "msSV7NptGswtM4k7Qom6f9efJ2rcZQQ8Ho", - }, -]; - -export const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; - -export const stakingInput = { - stakingAmountSat: 500_000, - finalityProviderPksNoCoordHex: [ - getPublicKeyNoCoord( - "d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76", - ), - ], - stakingTimelock: 64000, -}; - -export const utxos: UTXO[] = [ - { - scriptPubKey: - "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", - txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", - value: 8586757, - vout: 1, - }, -]; - -export const btcTipHeight = 900_000; -export const invalidStartHeightArr = [ - [0, "Babylon BTC tip height cannot be 0"], - [200_000, "Babylon params not found for height 200000"], -] as [number, string][]; - -export const feeRate = 4; - -export const stakingParams: VersionedStakingParams[] = [ - { - version: 0, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 5000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 64000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 857910, - }, - { - version: 1, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 50000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 864790, - }, - { - version: 2, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 874088, - }, - { - version: 3, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 891425, - }, - { - version: 4, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 9600, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 893362, - }, -].map((v) => ({ - version: v.version, - covenantNoCoordPks: v.covenant_pks.map((pk) => - String(getPublicKeyNoCoord(pk)), - ), - covenantQuorum: v.covenant_quorum, - minStakingValueSat: v.min_staking_value_sat, - maxStakingValueSat: v.max_staking_value_sat, - minStakingTimeBlocks: v.min_staking_time_blocks, - maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.unbonding_time_blocks, - unbondingFeeSat: v.unbonding_fee_sat, - minCommissionRate: v.min_commission_rate, - maxActiveFinalityProviders: v.max_active_finality_providers, - delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, - slashing: { - slashingPkScriptHex: v.slashing_pk_script, - slashingRate: parseFloat(v.slashing_rate), - minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, - }, - maxStakingAmountSat: v.max_staking_value_sat, - minStakingAmountSat: v.min_staking_value_sat, - btcActivationHeight: v.btc_activation_height, - allowListExpirationHeight: v.allow_list_expiration_height, -})); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts deleted file mode 100644 index e51d97c90c..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/providers.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const btcProvider = { - signPsbt: jest.fn(), - signMessage: jest.fn(), - getTransactionHex: jest.fn(), -}; - -export const babylonProvider = { - signTransaction: jest.fn(), - getCurrentHeight: jest.fn(), - getChainId: jest.fn(), -}; - -export const mockChainId = "bbn-1"; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts deleted file mode 100644 index 999e85853d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/registration.ts +++ /dev/null @@ -1,494 +0,0 @@ -import { Transaction } from "bitcoinjs-lib"; -import { - getPublicKeyNoCoord, - VersionedStakingParams, - type UTXO, -} from "../../../../src"; - -export const params: VersionedStakingParams[] = [ - { - version: 0, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 5000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 64000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 857910, - }, - { - version: 1, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 50000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 864790, - }, - { - version: 2, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 874088, - }, - { - version: 3, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 32000, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 891425, - }, - { - version: 4, - covenant_pks: [ - "d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaa", - "4b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9fa", - "23b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1", - "d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967ae", - "8242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7", - "e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41c", - "f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0", - "de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8c", - "cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204", - ], - covenant_quorum: 6, - min_staking_value_sat: 500000, - max_staking_value_sat: 500000000000, - min_staking_time_blocks: 64000, - max_staking_time_blocks: 64000, - slashing_pk_script: "6a07626162796c6f6e", - min_slashing_tx_fee_sat: 100000, - slashing_rate: "0.001000000000000000", - unbonding_time_blocks: 1008, - unbonding_fee_sat: 9600, - min_commission_rate: "0.030000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1095000, - allow_list_expiration_height: 139920, - btc_activation_height: 893362, - }, -].map((v) => ({ - version: v.version, - covenantNoCoordPks: v.covenant_pks.map((pk) => - String(getPublicKeyNoCoord(pk)), - ), - covenantQuorum: v.covenant_quorum, - minStakingValueSat: v.min_staking_value_sat, - maxStakingValueSat: v.max_staking_value_sat, - minStakingTimeBlocks: v.min_staking_time_blocks, - maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.unbonding_time_blocks, - unbondingFeeSat: v.unbonding_fee_sat, - minCommissionRate: v.min_commission_rate, - maxActiveFinalityProviders: v.max_active_finality_providers, - delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, - slashing: { - slashingPkScriptHex: v.slashing_pk_script, - slashingRate: parseFloat(v.slashing_rate), - minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, - }, - maxStakingAmountSat: v.max_staking_value_sat, - minStakingAmountSat: v.min_staking_value_sat, - btcActivationHeight: v.btc_activation_height, - allowListExpirationHeight: v.allow_list_expiration_height, -})); - -export const stakerInfo = { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", - ), - address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", -}; - -export const stakerInfoArr = [ - // Taproot - [ - stakerInfo, - { - slashingPsbt: - "70736274ff010070020000000197e5f77c011a657e5f3aa24d46c1b3e4949980a8e30b5d5555bfdbb929a7fae90000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012b20a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d0934215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac07ffc89a815b7b26da44c800e92dcf548694fd65486abe250fb6f7b30b73b2286fd7901200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - unbondingSlashingPsbt: - "70736274ff010070020000000151c1c42cbc4b725a5fa513ba3f10c1f6b5b6225f6446cd5ca61ea7e2e8dfdaea0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012ba07b070000000000225120655759c640a9d374e949e6e2cefdb6bee32e54b7dac9a0995fe508a04b3fd2cd4215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0010700255627c84b08e73ce57938ce8e6b01de0613e3a9dbb7216e9095d9129cfd7901200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - stakingTxHex: - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d09379627b0000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", - signType: "bip322-simple", - signedBabylonAddress: - "AUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", - signedSlashingPsbt: - "70736274ff010070020000000197e5f77c011a657e5f3aa24d46c1b3e4949980a8e30b5d5555bfdbb929a7fae90000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012b20a1070000000000225120745e0394730bd20a0a790069eeb28b4da95f73ea1d121374a299d8da9cb6d0930108fdff0103400fceade8b5e88c87305dda3a821e93916a0295c32fd9ad87e8b60fddc931a6aea4a78c690c979489a52dff8185c984d3e3a41bcd017d9633b2f744adb567d7f6fd7801200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac07ffc89a815b7b26da44c800e92dcf548694fd65486abe250fb6f7b30b73b2286000000", - signedUnbondingSlashingPsbt: - "70736274ff010070020000000151c1c42cbc4b725a5fa513ba3f10c1f6b5b6225f6446cd5ca61ea7e2e8dfdaea0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208c4b66479c64625efc30e0bc53c7df68173d3a444fdc0847e6a3ae4de1ab6add000000000001012ba07b070000000000225120655759c640a9d374e949e6e2cefdb6bee32e54b7dac9a0995fe508a04b3fd2cd0108fdff01034042d62d7dc274006463429df97d6e633dc98ea77f09ac3ce49d487fe06ac5beae2899b451891580cfa5a68f227aa63d15995dce710d44918a47fd1220525393a4fd7801200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0010700255627c84b08e73ce57938ce8e6b01de0613e3a9dbb7216e9095d9129c000000", - postStakingDelegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "BIP322", - btcSig: - "Cj50YjFwbHFnNDR3bHV3NjZ2cGtmY2N6MjNyZG10bGVwbngybTN5ZWY1N3l5ejY2ZmxneGRmNGg4cTd3dTZwZhJCAUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", - }, - btcPk: "CHSHYUf9dSLWF+g7+EX3+0mBUg48L3Sa1KLKG9Zg7ww=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIHReA5RzC9IKCnkAae6yi02pX3PqHRITdKKZ2NqcttCTeWJ7AAAAAAAiUSD4EVq7/Ha0wNk4wJURt2v+QzMrcSZTTxCC1pP0GamtzgAAAAA=", - stakingTxInclusionProof: { - key: { - index: 182, - hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", - }, - proof: - "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", - }, - slashingTx: - "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", - delegatorSlashingSig: - "D86t6LXojIcwXdo6gh6TkWoClcMv2a2H6LYP3ckxpq6kp4xpDJeUiaUt/4GFyYTT46QbzQF9ljOy90SttWfX9g==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wGgewcAAAAAACJRIGVXWcZAqdN06Unm4s79tr7jLlS32smgmV/lCKBLP9LNAAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAFRwcQsvEtyWl+lE7o/EMH2tbYiX2RGzVymHqfi6N/a6gAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", - delegatorUnbondingSlashingSig: - "QtYtfcJ0AGRjQp35fW5jPcmOp38JrDzknUh/4GrFvq4ombRRiRWAz6WmjyJ6pj0VmV3OcQ1EkYpH/RIgUlOTpA==", - }, - delegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "BIP322", - btcSig: - "Cj50YjFwbHFnNDR3bHV3NjZ2cGtmY2N6MjNyZG10bGVwbngybTN5ZWY1N3l5ejY2ZmxneGRmNGg4cTd3dTZwZhJCAUDG4E+rqWGwxtqAl3YuIY8vZ81qCbuLChpdQ7t0xxKpI8+TxXqeJzer8iNOtDbcKddhl8QDL5+1LQ70GsvEtF2t", - }, - btcPk: "CHSHYUf9dSLWF+g7+EX3+0mBUg48L3Sa1KLKG9Zg7ww=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIHReA5RzC9IKCnkAae6yi02pX3PqHRITdKKZ2NqcttCTeWJ7AAAAAAAiUSD4EVq7/Ha0wNk4wJURt2v+QzMrcSZTTxCC1pP0GamtzgAAAAA=", - slashingTx: - "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", - delegatorSlashingSig: - "D86t6LXojIcwXdo6gh6TkWoClcMv2a2H6LYP3ckxpq6kp4xpDJeUiaUt/4GFyYTT46QbzQF9ljOy90SttWfX9g==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAGX5fd8ARplfl86ok1GwbPklJmAqOMLXVVVv9u5Kaf66QAAAAAA/////wGgewcAAAAAACJRIGVXWcZAqdN06Unm4s79tr7jLlS32smgmV/lCKBLP9LNAAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAFRwcQsvEtyWl+lE7o/EMH2tbYiX2RGzVymHqfi6N/a6gAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIxLZkecZGJe/DDgvFPH32gXPTpET9wIR+ajrk3hq2rdAAAAAA==", - delegatorUnbondingSlashingSig: - "QtYtfcJ0AGRjQp35fW5jPcmOp38JrDzknUh/4GrFvq4ombRRiRWAz6WmjyJ6pj0VmV3OcQ1EkYpH/RIgUlOTpA==", - }, - }, - ], - // Native SegWit - [ - { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "03d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87", - ), - address: "tb1qlphktyz6sse3meq36pjwjrsqktny4553paydg2", - }, - { - slashingPsbt: - "70736274ff0100700200000001327d023e95b159b61998643f0c0f91ab2d4398e32b9030bcd7c6d80203f50d8f0000000000ffffffff02f401000000000000096a07626162796c6f6e8c18060000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012b20a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df334215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac043ed89d71b74d182f5dd7c29c9a85ff886dd243d000c4f02d019471fdf28be98fd79012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - unbondingSlashingPsbt: - "70736274ff01007002000000016864d6be973dd95a1efdf1832d93a1f15a481b08fa42a4efdcb8119155798fad0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f3050000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012ba07b0700000000002251209bfbdfbce192c6ad5ce1a9a5ec8f7b6d57d47a860de78cf9a5bcd0d061d8353f4215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08617477ba76d85cabf3d9e16ad2d85fe82d4ee485969a625cfe042932e91e0b6fd79012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - stakingTxHex: - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df3379627b0000000000160014f86f65905a84331de411d064e90e00b2e64ad29100000000", - signedBabylonAddress: - "AkgwRQIhAMxETaJ91QWSJOqFTECfvQMJID4gcIZ2DCUoRq0RrzeKAiAavtkp74/7B5u5N9T9g5l4/ZqRxXdsmdK4WeZga3rfhQEhAwPWeByOmsb9NT6XmX2QvvoIgsPgJ6cqsSr6ulw5HlqH", - signType: "bip322-simple", - signedSlashingPsbt: - "70736274ff0100700200000001327d023e95b159b61998643f0c0f91ab2d4398e32b9030bcd7c6d80203f50d8f0000000000ffffffff02f401000000000000096a07626162796c6f6e8c18060000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012b20a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df330108fdff010340f215a923800d21909fc14387d6f27495ae0c4beeba5804aafee5737ff5d33670777f3f93e653fd0cf11ba5987d46f704448320969df853481415af4e38c78c28fd78012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac043ed89d71b74d182f5dd7c29c9a85ff886dd243d000c4f02d019471fdf28be98000000", - signedUnbondingSlashingPsbt: - "70736274ff01007002000000016864d6be973dd95a1efdf1832d93a1f15a481b08fa42a4efdcb8119155798fad0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f3050000000000225120e366beca1d78015254028482014fd2589a13e158ad8796d89fa03fa1fee31ff7000000000001012ba07b0700000000002251209bfbdfbce192c6ad5ce1a9a5ec8f7b6d57d47a860de78cf9a5bcd0d061d8353f0108fdff01034058f5855f757e8e7b6913522d1118cad511cf17f6769875ba2260b989123cd7f450a537399859e3c817ac05c3938da076d28f3c385d4fcdd3856de30f4c288ea4fd78012003d6781c8e9ac6fd353e97997d90befa0882c3e027a72ab12afaba5c391e5a87ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08617477ba76d85cabf3d9e16ad2d85fe82d4ee485969a625cfe042932e91e0b6000000", - postStakingDelegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "BIP322", - btcSig: - "Cip0YjFxbHBoa3R5ejZzc2UzbWVxMzZwandqcnNxa3RueTQ1NTNwYXlkZzISbAJIMEUCIQDMRE2ifdUFkiTqhUxAn70DCSA+IHCGdgwlKEatEa83igIgGr7ZKe+P+webuTfU/YOZeP2akcV3bJnSuFnmYGt634UBIQMD1ngcjprG/TU+l5l9kL76CILD4CenKrEq+rpcOR5ahw==", - }, - btcPk: "A9Z4HI6axv01PpeZfZC++giCw+AnpyqxKvq6XDkeWoc=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIFd9W1+yieVJIBCYW5P9qNMlD5ez4vImCH1G2fGv9d8zeWJ7AAAAAAAWABT4b2WQWoQzHeQR0GTpDgCy5krSkQAAAAA=", - stakingTxInclusionProof: { - key: { - index: 182, - hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", - }, - proof: - "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", - }, - slashingTx: - "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", - delegatorSlashingSig: - "8hWpI4ANIZCfwUOH1vJ0la4MS+66WASq/uVzf/XTNnB3fz+T5lP9DPEbpZh9RvcERIMglp34U0gUFa9OOMeMKA==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wGgewcAAAAAACJRIJv737zhksatXOGppeyPe21X1HqGDeeM+aW80NBh2DU/AAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAFoZNa+lz3ZWh798YMtk6HxWkgbCPpCpO/cuBGRVXmPrQAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", - delegatorUnbondingSlashingSig: - "WPWFX3V+jntpE1ItERjK1RHPF/Z2mHW6ImC5iRI81/RQpTc5mFnjyBesBcOTjaB20o88OF1PzdOFbeMPTCiOpA==", - }, - delegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "BIP322", - btcSig: - "Cip0YjFxbHBoa3R5ejZzc2UzbWVxMzZwandqcnNxa3RueTQ1NTNwYXlkZzISbAJIMEUCIQDMRE2ifdUFkiTqhUxAn70DCSA+IHCGdgwlKEatEa83igIgGr7ZKe+P+webuTfU/YOZeP2akcV3bJnSuFnmYGt634UBIQMD1ngcjprG/TU+l5l9kL76CILD4CenKrEq+rpcOR5ahw==", - }, - btcPk: "A9Z4HI6axv01PpeZfZC++giCw+AnpyqxKvq6XDkeWoc=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIFd9W1+yieVJIBCYW5P9qNMlD5ez4vImCH1G2fGv9d8zeWJ7AAAAAAAWABT4b2WQWoQzHeQR0GTpDgCy5krSkQAAAAA=", - slashingTx: - "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", - delegatorSlashingSig: - "8hWpI4ANIZCfwUOH1vJ0la4MS+66WASq/uVzf/XTNnB3fz+T5lP9DPEbpZh9RvcERIMglp34U0gUFa9OOMeMKA==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAEyfQI+lbFZthmYZD8MD5GrLUOY4yuQMLzXxtgCA/UNjwAAAAAA/////wGgewcAAAAAACJRIJv737zhksatXOGppeyPe21X1HqGDeeM+aW80NBh2DU/AAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAFoZNa+lz3ZWh798YMtk6HxWkgbCPpCpO/cuBGRVXmPrQAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIONmvsodeAFSVAKEggFP0liaE+FYrYeW2J+gP6H+4x/3AAAAAA==", - delegatorUnbondingSlashingSig: - "WPWFX3V+jntpE1ItERjK1RHPF/Z2mHW6ImC5iRI81/RQpTc5mFnjyBesBcOTjaB20o88OF1PzdOFbeMPTCiOpA==", - }, - }, - ], - // Legacy - [ - { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "028333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37", - ), - address: "msSV7NptGswtM4k7Qom6f9efJ2rcZQQ8Ho", - }, - { - slashingPsbt: - "70736274ff010070020000000136fed8cea71d15ae6e4feda28f5658dd703d5bd20dfa9616dfb61b87b46578a20000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012b20a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d7414215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac074721ff8a756f465256499df328a3f3caba2cd9255953706a8ac424261d96536fd7901208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - unbondingSlashingPsbt: - "70736274ff0100700200000001d987aa78255e723474e10cfc7ac640432ef138bea619d579d1e28475446acf2c0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012ba07b070000000000225120eb99851e9f7dfdaa1e818a5c4146ee8c6d7683600fa0451e0da3794e1debff564215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac055355230be6141ba692e3ebbc0f553fb05a8160245890ecbceae6704830a9a90fd7901208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0000000", - stakingTxHex: - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d74179627b00000000001976a91482c9274701435286dc0bce950d9648878d466ad688ac00000000", - signedBabylonAddress: - "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", - signType: "ecdsa", - signedSlashingPsbt: - "70736274ff010070020000000136fed8cea71d15ae6e4feda28f5658dd703d5bd20dfa9616dfb61b87b46578a20000000000ffffffff02f401000000000000096a07626162796c6f6e8c180600000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012b20a10700000000002251201ed570c15555ca26344c8d1d5ee8ed8764a2869980839030c68f5dd71727d7410108fdff01034086ecbd50fecf3c86748f9504c858ebc3211f8e22cd70e2b96ca0910e0d9664610e84fbaa8447c8431cde3a05e890145c40f039133a98d50b94cabef65dec8e1dfd7801208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac074721ff8a756f465256499df328a3f3caba2cd9255953706a8ac424261d96536000000", - signedUnbondingSlashingPsbt: - "70736274ff0100700200000001d987aa78255e723474e10cfc7ac640432ef138bea619d579d1e28475446acf2c0000000000ffffffff02ea01000000000000096a07626162796c6f6e16f30500000000002251208bde60793a23f470a28e7f9b945a3d87e33f5e1d3253ed74f198762b14e92722000000000001012ba07b070000000000225120eb99851e9f7dfdaa1e818a5c4146ee8c6d7683600fa0451e0da3794e1debff560108fdff0103401b884aea88c45e4b557f1159cb7abb6e8d4f2b0af2221d9793e2bb8de801385141f82777662138cd87718a9246fca52e7d7484ec413b0376eaaad1cc28ba9077fd7801208333358d13582af186073cb3ad86c34630c186d7490603c4ce60fb51221c9a37ad20d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76ad2023b29f89b45f4af41588dcaf0ca572ada32872a88224f311373917f1b37d08d1ac204b15848e495a3a62283daaadb3f458a00859fe48e321f0121ebabbdd6698f9faba208242640732773249312c47ca7bdb50ca79f15f2ecc32b9c83ceebba44fb74df7ba20cbdd028cfe32c1c1f2d84bfec71e19f92df509bba7b8ad31ca6c1a134fe09204ba20d3c79b99ac4d265c2f97ac11e3232c07a598b020cf56c6f055472c893c0967aeba20d45c70d28f169e1f0c7f4a78e2bc73497afe585b70aa897955989068f3350aaaba20de13fc96ea6899acbdc5db3afaa683f62fe35b60ff6eb723dad28a11d2b12f8cba20e36200aaa8dce9453567bba108bdc51f7f1174b97a65e4dc4402fc5de779d41cba20f178fcce82f95c524b53b077e6180bd2d779a9057fdff4255a0af95af918cee0ba569c41c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac055355230be6141ba692e3ebbc0f553fb05a8160245890ecbceae6704830a9a90000000", - postStakingDelegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "ECDSA", - btcSig: - "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", - }, - btcPk: "gzM1jRNYKvGGBzyzrYbDRjDBhtdJBgPEzmD7USIcmjc=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIB7VcMFVVcomNEyNHV7o7YdkooaZgIOQMMaPXdcXJ9dBeWJ7AAAAAAAZdqkUgsknRwFDUobcC86VDZZIh41GataIrAAAAAA=", - stakingTxInclusionProof: { - key: { - index: 182, - hash: "kFJzEOcdOpI/twNJ9K/qsIXWbu0TDYOkTV29bIIAAAA=", - }, - proof: - "QmFz1hg6euFbROHG0FJwspulsAUBYJu6xLvFjewWxGy5Fyv+TjCQ61lgUQ9mi99On/Q4WB7olRoeI7dOhn4Vh3012zkV3b5WFA30MfHfO2T9gnrQiX1JSEWEsDZec3ZszEU5baFuvcl9sZD3zxgNdoAneVZU6CMdp63+v7domlrHUlVKngByzPuMsm8u8kP/TwRQgBXVvo1CIz1kF4jK+bCU6cAX7HGRNWZGE6Crkwya/eD0EqPCQPprXAthsNT3Oor7O3XpqZN6oHX18lZ5uF8I4QAhIm3uHd2nooKVbFk88z29qYnut37DZr+CT7PlmoVZblKCf7E2xtixv6QEwGNXV2LEjQTtNsG7mdK+R4KMW6m/81JXEnPhGMIiEzof", - }, - slashingTx: - "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", - delegatorSlashingSig: - "huy9UP7PPIZ0j5UEyFjrwyEfjiLNcOK5bKCRDg2WZGEOhPuqhEfIQxzeOgXokBRcQPA5EzqY1QuUyr72XeyOHQ==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wGgewcAAAAAACJRIOuZhR6fff2qHoGKXEFG7oxtdoNgD6BFHg2jeU4d6/9WAAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAHZh6p4JV5yNHThDPx6xkBDLvE4vqYZ1XnR4oR1RGrPLAAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", - delegatorUnbondingSlashingSig: - "G4hK6ojEXktVfxFZy3q7bo1PKwryIh2Xk+K7jegBOFFB+Cd3ZiE4zYdxipJG/KUufXSE7EE7A3bqqtHMKLqQdw==", - }, - delegationMsg: { - stakerAddr: "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - pop: { - btcSigType: "ECDSA", - btcSig: - "H+DSu5O5tgPBv1HUj+E8+KQSP3Guqdydr0LOTTcwJTldesgjEQyzvurLcVeliK3uXt7rjahIjK97JXBaoWVcgZc=", - }, - btcPk: "gzM1jRNYKvGGBzyzrYbDRjDBhtdJBgPEzmD7USIcmjc=", - fpBtcPkList: ["0jwsJeH8+P0cIbmkAsGeLjCeUx5F6S+x6YBbYFawzHY="], - stakingTime: 64000, - stakingValue: 500000, - stakingTx: - "AgAAAAHWbY1TPtw7zFxaWw7EsexxgHYSJs/mo4569I/iAoxqIgEAAAAA/////wIgoQcAAAAAACJRIB7VcMFVVcomNEyNHV7o7YdkooaZgIOQMMaPXdcXJ9dBeWJ7AAAAAAAZdqkUgsknRwFDUobcC86VDZZIh41GataIrAAAAAA=", - slashingTx: - "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wL0AQAAAAAAAAlqB2JhYnlsb26MGAYAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", - delegatorSlashingSig: - "huy9UP7PPIZ0j5UEyFjrwyEfjiLNcOK5bKCRDg2WZGEOhPuqhEfIQxzeOgXokBRcQPA5EzqY1QuUyr72XeyOHQ==", - unbondingTime: 1008, - unbondingTx: - "AgAAAAE2/tjOpx0Vrm5P7aKPVljdcD1b0g36lhbfthuHtGV4ogAAAAAA/////wGgewcAAAAAACJRIOuZhR6fff2qHoGKXEFG7oxtdoNgD6BFHg2jeU4d6/9WAAAAAA==", - unbondingValue: 490400, - unbondingSlashingTx: - "AgAAAAHZh6p4JV5yNHThDPx6xkBDLvE4vqYZ1XnR4oR1RGrPLAAAAAAA/////wLqAQAAAAAAAAlqB2JhYnlsb24W8wUAAAAAACJRIIveYHk6I/Rwoo5/m5RaPYfjP14dMlPtdPGYdisU6SciAAAAAA==", - delegatorUnbondingSlashingSig: - "G4hK6ojEXktVfxFZy3q7bo1PKwryIh2Xk+K7jegBOFFB+Cd3ZiE4zYdxipJG/KUufXSE7EE7A3bqqtHMKLqQdw==", - }, - }, - ], -] as const; - -export const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; - -export const stakingInput = { - stakingAmountSat: 500_000, - finalityProviderPksNoCoordHex: [ - getPublicKeyNoCoord( - "d23c2c25e1fcf8fd1c21b9a402c19e2e309e531e45e92fb1e9805b6056b0cc76", - ), - ], - stakingTimelock: 64000, -}; - -export const utxos: UTXO[] = [ - { - scriptPubKey: - "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", - txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", - value: 8586757, - vout: 1, - }, -]; - -export const btcTipHeight = 900_000; -export const invalidStartHeightArr = [ - [0, "Babylon BTC tip height cannot be 0"], - [800_000, "Babylon params not found for height 800000"], -] as [number, string][]; - -export const feeRate = 4; - -export const invalidBabylonAddresses = [ - "invalid-babylon-address", - "cosmos1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure", - "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8spure", - "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", - "cyqgpk0nlsutlm5ymkfpya30fqntanc8s", -]; - -export const stakingTx = Transaction.fromHex( - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff0220a1070000000000225120577d5b5fb289e5492010985b93fda8d3250f97b3e2f226087d46d9f1aff5df3379627b0000000000160014f86f65905a84331de411d064e90e00b2e64ad29100000000", -); - -export const inclusionProof = { - blockHashHex: - "000000826cbd5d4da4830d13ed6ed685b0eaaff44903b73f923a1de710735290", - merkle: [ - "6cc416ec8dc5bbc4ba9b600105b0a59bb27052d0c6e1445be17a3a18d6736142", - "87157e864eb7231e1a95e81e5838f49f4edf8b660f516059eb90304efe2b17b9", - "6c76735e36b0844548497d89d07a82fd643bdff131f40d1456bedd1539db357d", - "5a9a68b7bffeada71d23e85456792780760d18cff790b17dc9bd6ea16d3945cc", - "f9ca8817643d23428dbed5158050044fff43f22e6fb28cfbcc72009e4a5552c7", - "f7d4b0610b5c6bfa40c2a312f4e0fd9a0c93aba0134666359171ec17c0e994b0", - "596c9582a2a7dd1dee6d222100e1085fb87956f2f575a07a93a9e9753bfb8a3a", - "c004a4bfb1d8c636b17f82526e59859ae5b34f82bf66c37eb7ee89a9bd3df33c", - "1f3a1322c218e173125752f3bfa95b8c8247bed299bbc136ed048dc462575763", - ], - pos: 182, -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts deleted file mode 100644 index f1067c95a3..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/staking.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { getPublicKeyNoCoord, UTXO } from "../../../../src"; - -export const params = [ - { - version: 0, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 222170, - }, - { - version: 1, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 100000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227443, - }, - { - version: 2, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 5, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227490, - }, -].map((v) => ({ - version: v.version, - covenantNoCoordPks: v.covenant_pks.map((pk) => - String(getPublicKeyNoCoord(pk)), - ), - covenantQuorum: v.covenant_quorum, - minStakingValueSat: v.min_staking_value_sat, - maxStakingValueSat: v.max_staking_value_sat, - minStakingTimeBlocks: v.min_staking_time_blocks, - maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.unbonding_time_blocks, - unbondingFeeSat: v.unbonding_fee_sat, - minCommissionRate: v.min_commission_rate, - maxActiveFinalityProviders: v.max_active_finality_providers, - delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, - slashing: { - slashingPkScriptHex: v.slashing_pk_script, - slashingRate: parseFloat(v.slashing_rate), - minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, - }, - maxStakingAmountSat: v.max_staking_value_sat, - minStakingAmountSat: v.min_staking_value_sat, - btcActivationHeight: v.btc_activation_height, - allowListExpirationHeight: v.allow_list_expiration_height, -})); - -export const stakerInfo = { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", - ), - address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", -}; - -export const utxos: UTXO[] = [ - { - scriptPubKey: - "5120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce", - txid: "226a8c02e28ff47a8ea3e6cf2612768071ecb1c40e5b5a5ccc3bdc3e538d6dd6", - value: 8586757, - vout: 1, - }, -]; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts deleted file mode 100644 index 1e6f96eab1..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/unbonding.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Transaction } from "bitcoinjs-lib"; -import { getPublicKeyNoCoord } from "../../../../src"; - -export const params = [ - { - version: 0, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 222170, - }, - { - version: 1, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 100000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227443, - }, - { - version: 2, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 5, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227490, - }, -].map((v) => ({ - version: v.version, - covenantNoCoordPks: v.covenant_pks.map((pk) => - String(getPublicKeyNoCoord(pk)), - ), - covenantQuorum: v.covenant_quorum, - minStakingValueSat: v.min_staking_value_sat, - maxStakingValueSat: v.max_staking_value_sat, - minStakingTimeBlocks: v.min_staking_time_blocks, - maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.unbonding_time_blocks, - unbondingFeeSat: v.unbonding_fee_sat, - minCommissionRate: v.min_commission_rate, - maxActiveFinalityProviders: v.max_active_finality_providers, - delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, - slashing: { - slashingPkScriptHex: v.slashing_pk_script, - slashingRate: parseFloat(v.slashing_rate), - minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, - }, - maxStakingAmountSat: v.max_staking_value_sat, - minStakingAmountSat: v.min_staking_value_sat, - btcActivationHeight: v.btc_activation_height, - allowListExpirationHeight: v.allow_list_expiration_height, -})); - -export const stakingTx = Transaction.fromHex( - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", -); - -export const stakingInput = { - stakingAmountSat: 11_000, - finalityProviderPksNoCoordHex: [ - getPublicKeyNoCoord( - "02eb83395c33cf784f7dfb90dcc918b5620ddd67fe6617806f079322dc4db2f0", - ), - ], - stakingTimelock: 100, -}; - -export const version = 2; - -export const covenantUnbondingSignatures = [ - { - btcPkHex: - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - sigHex: - "6bcfc07a4b0caa6f047821e6553bad8a4e3a8f134d41619566a8f2b926ea1fa838d4a098eb2ea8516bc1e6f4ea53d23b6af3acc14b9dfb5fbcb57a9756e32606", - }, - { - btcPkHex: - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - sigHex: - "45f23ff78495e8d35b06b504017ff0f57c6d1a48878359675fd51e2e52570910a0e61439761cddcb4a5956a333a943c4937ff13514dd582cdf48435066311f17", - }, - { - btcPkHex: - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - sigHex: - "4cc1246632df302ce78cb374de5d153df107a227d726ec81ec3de49b72cc47d3a0cca3a3ee38d74823390b758f0a451e8b5bb98068e7814ce2664532cba80436", - }, -]; - -export const unbondingPsbt = - "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a36215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141568b200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529cc001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; - -export const stakerInfo = { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", - ), - address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts deleted file mode 100644 index 93c5e2d691..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/__mock__/withdrawal.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Transaction } from "bitcoinjs-lib"; -import { getPublicKeyNoCoord } from "../../../../src"; - -export const stakerInfo = { - publicKeyNoCoordHex: getPublicKeyNoCoord( - "0874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c", - ), - address: "tb1plqg44wluw66vpkfccz23rdmtlepnx2m3yef57yyz66flgxdf4h8q7wu6pf", -}; - -export const unboundingTx = Transaction.fromHex( - "0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d10000000000ffffffff011c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd31600000000", -); - -export const stakingTx = Transaction.fromHex( - "02000000013ceeae53363582ad438aa20b0e95917b01d8eb8c15b030f5cbfcd90587dfaf720100000000ffffffff021027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf322b810000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", -); - -export const slashingTx = Transaction.fromHex( - "0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d10000000000ffffffff02e803000000000000160014f87283ca2ab20a1ab50cc7cea290f722c9a24574401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac400000000", -); - -export const stakingInput = { - stakingAmountSat: 10_000, - finalityProviderPksNoCoordHex: [ - getPublicKeyNoCoord( - "bb762e89f88a060707371b06fb13a896c1adab058df6a25e35463c14c82eca70", - ), - ], - stakingTimelock: 100, -}; - -export const version = 2; - -export const params = [ - { - version: 0, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 222170, - }, - { - version: 1, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 100000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 20, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227443, - }, - { - version: 2, - covenant_pks: [ - "ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5", - "a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31", - "59d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4", - ], - covenant_quorum: 2, - min_staking_value_sat: 10000, - max_staking_value_sat: 1000000000000, - min_staking_time_blocks: 100, - max_staking_time_blocks: 60000, - slashing_pk_script: "0014f87283ca2ab20a1ab50cc7cea290f722c9a24574", - min_slashing_tx_fee_sat: 1000, - slashing_rate: "0.100000000000000000", - unbonding_time_blocks: 5, - unbonding_fee_sat: 500, - min_commission_rate: "0.050000000000000000", - max_active_finality_providers: 0, - delegation_creation_base_gas_fee: 1000000, - allow_list_expiration_height: 1440, - btc_activation_height: 227490, - }, -].map((v) => ({ - version: v.version, - covenantNoCoordPks: v.covenant_pks.map((pk) => - String(getPublicKeyNoCoord(pk)), - ), - covenantQuorum: v.covenant_quorum, - minStakingValueSat: v.min_staking_value_sat, - maxStakingValueSat: v.max_staking_value_sat, - minStakingTimeBlocks: v.min_staking_time_blocks, - maxStakingTimeBlocks: v.max_staking_time_blocks, - unbondingTime: v.unbonding_time_blocks, - unbondingFeeSat: v.unbonding_fee_sat, - minCommissionRate: v.min_commission_rate, - maxActiveFinalityProviders: v.max_active_finality_providers, - delegationCreationBaseGasFee: v.delegation_creation_base_gas_fee, - slashing: { - slashingPkScriptHex: v.slashing_pk_script, - slashingRate: parseFloat(v.slashing_rate), - minSlashingTxFeeSat: v.min_slashing_tx_fee_sat, - }, - maxStakingAmountSat: v.max_staking_value_sat, - minStakingAmountSat: v.min_staking_value_sat, - btcActivationHeight: v.btc_activation_height, - allowListExpirationHeight: v.allow_list_expiration_height, -})); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts deleted file mode 100644 index c000c3c261..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/fee.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { networks } from "bitcoinjs-lib"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { - btcTipHeight, - feeRate, - invalidStartHeightArr, - stakerInfo, - stakerInfoArr, - stakingInput, - stakingParams, - utxos, -} from "./__mock__/fee"; -import { babylonProvider, btcProvider } from "./__mock__/providers"; - -describe("Staking Manager", () => { - describe("estimateBtcStakingFee", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - stakingParams, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - }); - - it.each(invalidStartHeightArr)( - "should validate babylonBtcTipHeight", - async (btcTipHeight, errorMessage) => { - try { - await manager.estimateBtcStakingFee( - stakerInfo, - btcTipHeight, - stakingInput, - utxos, - feeRate, - ); - } catch (e: any) { - expect(e.message).toMatch(errorMessage); - } - }, - ); - - it("should validate babylonBtcTipHeight", async () => { - const btcTipHeight = 100; - - try { - await manager.estimateBtcStakingFee( - stakerInfo, - btcTipHeight, - stakingInput, - utxos, - feeRate, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for height ${btcTipHeight}`, - ); - } - }); - - it.each(stakerInfoArr)("should return valid tx fee", async (stakerInfo) => { - const txFee = await manager.estimateBtcStakingFee( - stakerInfo, - btcTipHeight, - stakingInput, - utxos, - feeRate, - ); - - expect(txFee).toEqual(620); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts deleted file mode 100644 index 57bf0c5618..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/init.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { networks } from "bitcoinjs-lib"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { params } from "./__mock__/staking"; - -describe("Staking Manager", () => { - describe("Initialization", () => { - it("should succesfully initialize Staking Manager", () => { - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - ); - - expect(manager).toBeDefined(); - }); - - it("should throw an init error", () => { - expect( - () => - new BabylonBtcStakingManager( - networks.bitcoin, - [], - btcProvider, - babylonProvider, - ), - ).toThrow("No staking parameters provided"); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts deleted file mode 100644 index b6f9933173..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/pop.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { networks } from "bitcoinjs-lib"; -import { sha256 } from "bitcoinjs-lib/src/crypto"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; -import { STAKING_MODULE_ADDRESS } from "../../../src/constants/staking"; - -import { babylonProvider, btcProvider, mockChainId } from "./__mock__/providers"; -import { params, stakerInfo } from "./__mock__/staking"; -import { babylonAddress } from "./__mock__/fee"; - -describe("Staking Manager - POP Integration", () => { - const mockBech32Address = babylonAddress; - const mockBtcAddress = stakerInfo.address; - - beforeEach(() => { - jest.clearAllMocks(); - btcProvider.signMessage.mockResolvedValue("mocked-signature"); - babylonProvider.getChainId.mockResolvedValue(mockChainId); - }); - - describe("Legacy POP Format (Below Upgrade Height)", () => { - it("should use legacy format when height is below upgrade height", async () => { - const mockGetCurrentHeight = jest.fn().mockResolvedValue(100); - babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); - - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - undefined, - { - pop: upgradeConfig, - }, - ); - - await manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockBtcAddress, - ); - - // Should sign just the bech32 address (legacy format) - expect(btcProvider.signMessage).toHaveBeenCalledWith( - mockBech32Address, - "ecdsa", - ); - expect(mockGetCurrentHeight).toHaveBeenCalled(); - }); - - it("should use legacy format when no upgrade options provided and optional babylon provider methods are not provided", async () => { - babylonProvider.getCurrentHeight.mockResolvedValue(undefined); - babylonProvider.getChainId.mockResolvedValue(undefined); - - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - ); - - await manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockBtcAddress, - ); - - // Should sign just the bech32 address (legacy format) - expect(btcProvider.signMessage).toHaveBeenCalledWith( - mockBech32Address, - "ecdsa", - ); - }); - }); - - describe("New POP Format (Above Upgrade Height)", () => { - it("should use new format when height is above upgrade height", async () => { - const mockGetCurrentHeight = jest.fn().mockResolvedValue(300); - babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); - - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - undefined, - { - pop: upgradeConfig, - }, - ); - - await manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockBtcAddress, - ); - - // Calculate expected message with context hash - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(btcProvider.signMessage).toHaveBeenCalledWith( - expectedMessage, - "ecdsa", - ); - expect(mockGetCurrentHeight).toHaveBeenCalled(); - }); - - it("should use new format when popContextUpgradeHeight is 0 (always use new format)", async () => { - const mockGetCurrentHeight = jest.fn().mockResolvedValue(100); - babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); - - const upgradeConfig = { - upgradeHeight: 0, - version: 0, - }; - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - undefined, - { - pop: upgradeConfig, - }, - ); - - await manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockBtcAddress, - ); - - // Calculate expected message with context hash - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(btcProvider.signMessage).toHaveBeenCalledWith( - expectedMessage, - "ecdsa", - ); - }); - }); - - describe("Error Handling", () => { - it("should throw error when height detection fails", async () => { - const mockGetCurrentHeight = jest - .fn() - .mockRejectedValue(new Error("Network error")); - babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - undefined, - { - pop: upgradeConfig, - }, - ); - - await expect( - manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockBtcAddress, - ), - ).rejects.toThrow("Network error"); - }); - }); - - describe("BIP322 Support", () => { - it("should use BIP322 signature for taproot addresses with new format", async () => { - const mockTaprootAddress = - "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"; - const mockGetCurrentHeight = jest.fn().mockResolvedValue(300); - babylonProvider.getCurrentHeight.mockImplementation(mockGetCurrentHeight); - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - - const manager = new BabylonBtcStakingManager( - networks.bitcoin, - params, - btcProvider, - babylonProvider, - undefined, - { - pop: upgradeConfig, - }, - ); - - await manager.createProofOfPossession( - "delegation:create", - mockBech32Address, - mockTaprootAddress, - ); - - // Calculate expected message with context hash - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(btcProvider.signMessage).toHaveBeenCalledWith( - expectedMessage, - "bip322-simple", - ); - }); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts deleted file mode 100644 index 75a1a2ee59..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/postStaking.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { networks, Transaction } from "bitcoinjs-lib"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; -import { ContractId } from "../../../src/types/contract"; -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { - babylonAddress, - btcTipHeight, - inclusionProof, - invalidBabylonAddresses, - invalidStartHeightArr, - params, - stakerInfo, - stakerInfoArr, - stakingInput, - stakingTx, -} from "./__mock__/registration"; -import { ActionName } from "../../../src/types/action"; - -describe("Staking Manager", () => { - describe("postStakeRegistrationBabylonTransaction", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - params, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - btcProvider.signMessage.mockReset(); - babylonProvider.signTransaction.mockReset(); - }); - - it.each(invalidStartHeightArr)( - "should validate babylonBtcTipHeight %s", - async (btcTipHeight) => { - try { - await manager.postStakeRegistrationBabylonTransaction( - stakerInfo, - stakingTx, - btcTipHeight, - stakingInput, - inclusionProof, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for height ${btcTipHeight}`, - ); - } - }, - ); - - it.each(invalidBabylonAddresses)( - "should validate babylon address", - async (babylonAddress) => { - try { - await manager.postStakeRegistrationBabylonTransaction( - stakerInfo, - stakingTx, - btcTipHeight, - stakingInput, - inclusionProof, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch("Invalid Babylon address"); - } - }, - ); - - it("should validate tx output", async () => { - const tx = { - ...stakingTx, - outs: [], - } as any; - - try { - await manager.postStakeRegistrationBabylonTransaction( - stakerInfo, - tx, - btcTipHeight, - stakingInput, - inclusionProof, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch(/Matching output not found for address:/); - } - }); - - it.each(stakerInfoArr)( - "should create valid pre stake registration tx", - async ( - stakerInfo, - { - slashingPsbt, - unbondingSlashingPsbt, - signedSlashingPsbt, - signedUnbondingSlashingPsbt, - signedBabylonAddress, - stakingTxHex, - postStakingDelegationMsg, - signType, - }, - ) => { - const version = 4; - - btcProvider.signPsbt - .mockResolvedValueOnce(signedSlashingPsbt) - .mockResolvedValueOnce(signedUnbondingSlashingPsbt); - btcProvider.signMessage.mockResolvedValueOnce(signedBabylonAddress); - - await manager.postStakeRegistrationBabylonTransaction( - stakerInfo, - Transaction.fromHex(stakingTxHex), - btcTipHeight, - stakingInput, - inclusionProof, - babylonAddress, - ); - - expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { - contracts: [ - { - id: ContractId.STAKING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - minUnbondingTime: params[version].unbondingTime, - stakingDuration: stakingInput.stakingTimelock, - }, - }, - { - id: ContractId.SLASHING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - unbondingTimeBlocks: params[version].unbondingTime, - slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, - }, - }, - { - id: ContractId.SLASHING_BURN, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, - }, - }); - expect(btcProvider.signPsbt).toHaveBeenCalledWith( - unbondingSlashingPsbt, - { - contracts: [ - { - id: ContractId.UNBONDING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - unbondingTimeBlocks: params[version].unbondingTime, - unbondingFeeSat: params[version].unbondingFeeSat, - }, - }, - { - id: ContractId.SLASHING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - unbondingTimeBlocks: params[version].unbondingTime, - slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, - }, - }, - { - id: ContractId.SLASHING_BURN, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, - }, - }, - ); - expect(btcProvider.signMessage).toHaveBeenCalledWith( - babylonAddress, - signType, - ); - expect( - btcstakingtx.MsgCreateBTCDelegation.toJSON( - babylonProvider.signTransaction.mock.calls[0][0].value, - ), - ).toEqual(postStakingDelegationMsg); - }, - ); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts deleted file mode 100644 index 62d5adeffb..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/preStaking.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; -import { networks } from "bitcoinjs-lib"; - -import { type UTXO } from "../../../src"; -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { ContractId } from "../../../src/types/contract"; -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { - babylonAddress, - btcTipHeight, - feeRate, - invalidBabylonAddresses, - invalidStartHeightArr, - params, - stakerInfo, - stakerInfoArr, - stakingInput, - utxos, -} from "./__mock__/registration"; -import { ActionName } from "../../../src/types/action"; - -describe("Staking Manager", () => { - describe("preStakeRegistrationBabylonTransaction", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - params, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - btcProvider.signMessage.mockReset(); - babylonProvider.signTransaction.mockReset(); - }); - - it.each(invalidStartHeightArr)( - "should validate babylonBtcTipHeight", - async (btcTipHeight, errorMessage) => { - try { - await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - btcTipHeight, - utxos, - feeRate, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch(errorMessage); - } - }, - ); - - it("should validate input UTXOs", async () => { - const utxos: UTXO[] = []; - - try { - await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - btcTipHeight, - utxos, - feeRate, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch("No input UTXOs provided"); - } - }); - - it.each(invalidBabylonAddresses)( - "should validate babylon address", - async (babylonAddress) => { - try { - await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - btcTipHeight, - utxos, - feeRate, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch("Invalid Babylon address"); - } - }, - ); - - it("should validate babylon params", async () => { - const btcTipHeight = 100; - - try { - await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - btcTipHeight, - utxos, - feeRate, - babylonAddress, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for height ${btcTipHeight}`, - ); - } - }); - - it.each(stakerInfoArr)( - "should create valid pre stake registration tx", - async ( - stakerInfo, - { - signedSlashingPsbt, - signedUnbondingSlashingPsbt, - signedBabylonAddress, - slashingPsbt, - unbondingSlashingPsbt, - delegationMsg, - stakingTxHex, - signType, - }, - ) => { - const version = 4; - - btcProvider.signPsbt - .mockResolvedValueOnce(signedSlashingPsbt) - .mockResolvedValueOnce(signedUnbondingSlashingPsbt); - btcProvider.signMessage.mockResolvedValueOnce(signedBabylonAddress); - - const { stakingTx } = - await manager.preStakeRegistrationBabylonTransaction( - stakerInfo, - stakingInput, - btcTipHeight, - utxos, - feeRate, - babylonAddress, - ); - - expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { - contracts: [ - { - id: ContractId.STAKING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - minUnbondingTime: params[version].unbondingTime, - stakingDuration: stakingInput.stakingTimelock, - }, - }, - { - id: ContractId.SLASHING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - unbondingTimeBlocks: params[version].unbondingTime, - slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, - }, - }, - { - id: ContractId.SLASHING_BURN, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, - }, - }); - expect(btcProvider.signPsbt).toHaveBeenCalledWith( - unbondingSlashingPsbt, - { - contracts: [ - { - id: ContractId.UNBONDING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - unbondingTimeBlocks: params[version].unbondingTime, - unbondingFeeSat: params[version].unbondingFeeSat, - }, - }, - { - id: ContractId.SLASHING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - unbondingTimeBlocks: params[version].unbondingTime, - slashingFeeSat: params[version].slashing?.minSlashingTxFeeSat, - }, - }, - { - id: ContractId.SLASHING_BURN, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - slashingPkScriptHex: params[version].slashing?.slashingPkScriptHex, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, - }, - }, - ); - expect(btcProvider.signMessage).toHaveBeenCalledWith( - babylonAddress, - signType, - ); - expect( - btcstakingtx.MsgCreateBTCDelegation.toJSON( - babylonProvider.signTransaction.mock.calls[0][0].value, - ), - ).toEqual(delegationMsg); - expect(stakingTx.toHex()).toBe(stakingTxHex); - }, - ); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts deleted file mode 100644 index 3f7663f1b7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/staking.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { networks, Psbt, Transaction } from "bitcoinjs-lib"; - -import { getPublicKeyNoCoord, type UTXO } from "../../../src"; -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { params, stakerInfo, utxos } from "./__mock__/staking"; -import { ContractId } from "../../../src/types/contract"; -import { ActionName } from "../../../src/types/action"; - -const unsignedStakingTx = Transaction.fromHex( - "0200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce00000000", -); -const stakingInput = { - stakingAmountSat: 11_000, - finalityProviderPksNoCoordHex: [ - getPublicKeyNoCoord( - "02eb83395c33cf784f7dfb90dcc918b5620ddd67fe6617806f079322dc4db2f0", - ), - ], - stakingTimelock: 100, -}; -const version = 2; - -describe("Staking Manager", () => { - describe("createSignedBtcStakingTransaction", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - params, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - }); - - it("should validate version params", async () => { - const version = 5; - - try { - await manager.createSignedBtcStakingTransaction( - stakerInfo, - stakingInput, - unsignedStakingTx, - utxos, - version, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should validate input utxos", async () => { - const utxos: UTXO[] = []; - - try { - await manager.createSignedBtcStakingTransaction( - stakerInfo, - stakingInput, - unsignedStakingTx, - utxos, - version, - ); - } catch (e: any) { - expect(e.message).toMatch("No input UTXOs provided"); - } - }); - - it("should sign staking tx", async () => { - const unsignedTx = - "70736274ff0100890200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b0506830000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce0117200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0c000000"; - const signedTx = - "70736274ff0100890200000001d66d8d533edc3bcc5c5a5b0ec4b1ec7180761226cfe6a38e7af48fe2028c6a220100000000ffffffff02f82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a354da820000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b0506830000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce01084201404039aac248eb40aaab21fd58556ad7d81e177df5119c7225b1e881fc6c64c106d2ba95865895050df5fe64a86f81f3273687056f97fd506c792ebce281d8dbb0000000"; - btcProvider.signPsbt.mockResolvedValueOnce(signedTx); - - const tx = await manager.createSignedBtcStakingTransaction( - stakerInfo, - stakingInput, - unsignedStakingTx, - utxos, - version, - ); - - expect(btcProvider.signPsbt).toHaveBeenLastCalledWith(unsignedTx, { - contracts: [ - { - id: ContractId.STAKING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - minUnbondingTime: params[version].unbondingTime, - stakingDuration: stakingInput.stakingTimelock, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_STAKING_TRANSACTION, - }, - }); - expect(tx).toEqual(Psbt.fromHex(signedTx).extractTransaction()); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts deleted file mode 100644 index 269527edb1..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/unbonding.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { networks, Psbt, Transaction } from "bitcoinjs-lib"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { ContractId } from "../../../src/types/contract"; -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { - covenantUnbondingSignatures, - params, - stakerInfo, - stakingInput, - stakingTx, - unbondingPsbt, - version, -} from "./__mock__/unbonding"; -import { ActionName } from "../../../src/types/action"; - -describe("Staking Manager", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - params, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - }); - - describe("createPartialSignedBtcUnbondingTransaction", () => { - it("should validate version params", async () => { - const version = 5; - - try { - await manager.createPartialSignedBtcUnbondingTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should create partial signed unbonding tx", async () => { - const signedUnbondingTx = - "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; - btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); - - const { transaction, fee } = - await manager.createPartialSignedBtcUnbondingTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - ); - - expect(btcProvider.signPsbt).toHaveBeenLastCalledWith(unbondingPsbt, { - contracts: [ - { - id: ContractId.STAKING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - minUnbondingTime: params[version].unbondingTime, - stakingDuration: stakingInput.stakingTimelock, - }, - }, - { - id: ContractId.UNBONDING, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - finalityProviders: stakingInput.finalityProviderPksNoCoordHex, - covenantPks: params[version].covenantNoCoordPks, - covenantThreshold: params[version].covenantQuorum, - unbondingTimeBlocks: params[version].unbondingTime, - unbondingFeeSat: params[version].unbondingFeeSat, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_UNBONDING_TRANSACTION, - }, - }); - expect(transaction).toEqual( - Psbt.fromHex(signedUnbondingTx).extractTransaction(), - ); - expect(fee).toEqual(500); - }); - }); - - describe("createSignedBtcUnbondingTransaction", () => { - it("should validate version params", async () => { - const version = 5; - const unbondingTx = Transaction.fromHex( - "02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b94400000000", - ); - - try { - await manager.createSignedBtcUnbondingTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - unbondingTx, - covenantUnbondingSignatures, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should validate unbonding tx", async () => { - const signedUnbondingTx = - "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; - const unbondingTx = Transaction.fromHex( - "02000000013ceeae53363582ad438aa20b0e95917b01d8eb8c15b030f5cbfcd90587dfaf720000000000ffffffff01ac84010000000000225120453e6f3b8f487fb51b9e598e08eb83febb6e6d0d749c883b1696bbe1a269722600000000", - ); - btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); - - try { - await manager.createSignedBtcUnbondingTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - unbondingTx, - covenantUnbondingSignatures, - ); - } catch (e: any) { - expect(e.message).toMatch( - "Unbonding transaction hash does not match the computed hash", - ); - } - }); - - it("should validate version params", async () => { - const signedUnbondingTx = - "70736274ff01005e02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944000000000001012bf82a000000000000225120c3177fd7052d79a2d50a5c60217f0b5855371fe5f9a5322bafa8fcd24a3c31a30108fd2f010340beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab141560000"; - const unbondingTx = Transaction.fromHex( - "02000000011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b94400000000", - ); - btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingTx); - - const { transaction, fee } = - await manager.createSignedBtcUnbondingTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - unbondingTx, - covenantUnbondingSignatures, - ); - - expect(fee).toEqual(500); - expect(transaction.toHex()).toBe( - "020000000001011e70a47d4ad5d4b67f428797805d888a0bf8bc74bbf6a34f6651b4765524d4c60000000000ffffffff01042900000000000022512084a0af8755a320a6cd0d7d12192322c716a71ce50831316733a276baf649b944064045f23ff78495e8d35b06b504017ff0f57c6d1a48878359675fd51e2e52570910a0e61439761cddcb4a5956a333a943c4937ff13514dd582cdf48435066311f17406bcfc07a4b0caa6f047821e6553bad8a4e3a8f134d41619566a8f2b926ea1fa838d4a098eb2ea8516bc1e6f4ea53d23b6af3acc14b9dfb5fbcb57a9756e326060040beff4acba24751a509a56ce297ad6726fb3c8b8d3ec83113b2700d58217f2d9d99810d46f6a2ac74e863522e22a11523cf2d176db1c40ddc8f98951b380c96768a200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad2059d3532148a597a2d05c0395bf5f7176044b1cd312f37701a9b4d0aad70bc5a4ac20a5c60c2188e833d39d0fa798ab3f69aa12ed3dd2f3bad659effa252782de3c31ba20ffeaec52a9b407b355ef6967a7ffc15fd6c3fe07de2844d61550475e7a5233e5ba529c61c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0822a15c402bc3de196e9dfe6d4bcf9b55978f4da73fb0b18ebc083136ee58a3baf6b354e2c079c6d444ef391f391ece3b06e354895586ccb9847aa6a0ab1415600000000", - ); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts deleted file mode 100644 index 648a7a8946..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/manager/withdrawal.test.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { networks, Psbt } from "bitcoinjs-lib"; - -import { BabylonBtcStakingManager } from "../../../src/staking/manager"; - -import { babylonProvider, btcProvider } from "./__mock__/providers"; -import { - params, - slashingTx, - stakerInfo, - stakingInput, - stakingTx, - unboundingTx, - version, -} from "./__mock__/withdrawal"; -import { ContractId } from "../../../src/types/contract"; -import { ActionName } from "../../../src/types/action"; - -describe("Staking Manager", () => { - describe("Create Withdrawal Transaction", () => { - let manager: BabylonBtcStakingManager; - - beforeEach(() => { - manager = new BabylonBtcStakingManager( - networks.testnet, - params, - btcProvider, - babylonProvider, - ); - }); - - afterEach(() => { - btcProvider.signPsbt.mockReset(); - }); - - // Early Unbonded - describe("createSignedBtcWithdrawEarlyUnbondedTransaction", () => { - it("should validate version params", async () => { - const version = 5; - - try { - await manager.createSignedBtcWithdrawEarlyUnbondedTransaction( - stakerInfo, - stakingInput, - version, - unboundingTx, - 4, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should create withdrawal tx", async () => { - const unbondingPsbt = - "70736274ff01005e020000000157b41412aa878b560867e9e97b52247555a7016a679e59dc5084c728ecbe7091000000000005000000011823000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd3164215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd25200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; - const signedUnbondingPsbt = - "70736274ff01005e020000000157b41412aa878b560867e9e97b52247555a7016a679e59dc5084c728ecbe7091000000000005000000011823000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1c2500000000000022512006d056d1b3d0907ad731d3bc4e5960d6640ba606f107db6e3520757ae09cd3160108a903405ff651c35d926db0c6a93e8852cb6d3d59e0355a2c6f09aaef817e406887fe9e8f4f05b64e0efab47ab8670e8b8891e8df60a43eb402cc63197a224890c3ed1f24200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b241c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd0000"; - btcProvider.signPsbt.mockResolvedValueOnce(signedUnbondingPsbt); - - const { transaction, fee } = - await manager.createSignedBtcWithdrawEarlyUnbondedTransaction( - stakerInfo, - stakingInput, - version, - unboundingTx, - 4, - ); - - expect(btcProvider.signPsbt).toHaveBeenCalledWith(unbondingPsbt, { - contracts: [ - { - id: ContractId.WITHDRAW, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - timelockBlocks: params[version].unbondingTime, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, - }); - expect(transaction.toHex()).toBe( - Psbt.fromHex(signedUnbondingPsbt).extractTransaction().toHex(), - ); - expect(fee).toEqual(516); - }); - }); - - // Staking Expired - describe("createSignedBtcWithdrawStakingExpiredTransaction", () => { - it("should validate version params", async () => { - const version = 5; - - try { - await manager.createSignedBtcWithdrawStakingExpiredTransaction( - stakerInfo, - stakingInput, - version, - stakingTx, - 4, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should create withdrawal tx", async () => { - const withdrawPsbt = - "70736274ff01005e0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d1000000000064000000010c25000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf6215c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08317b993160b148500d8fc1f6520c7f8ead4ae9537268552dcc20238fb6bb3f5802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd26200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad0164b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; - const signedwithdrawPsbt = - "70736274ff01005e0200000001260d8608c71a9dbe5573a2d25450bc1830c7ace5a9615016e1e5dabac32af0d1000000000064000000010c25000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b1027000000000000225120de38b90b3e98822941d246c36859553591477a0b0eeb25a5bcda525b98849ecf0108ca0340726cd5ac48fe5728720d4179a68d4fe59076762f980626a82bc83c9d89c750364b4cd9d0a5acbf9259e2fe3977c7becc5e16b24dde88fe2792095b23ada1ffdf25200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad0164b261c050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac08317b993160b148500d8fc1f6520c7f8ead4ae9537268552dcc20238fb6bb3f5802f5c2c331475f980037e78432f190e3d30cedf9b61556eba388ab7faad9dbd0000"; - btcProvider.signPsbt.mockResolvedValueOnce(signedwithdrawPsbt); - - const { transaction, fee } = - await manager.createSignedBtcWithdrawStakingExpiredTransaction( - stakerInfo, - stakingInput, - version, - - stakingTx, - 4, - ); - - expect(btcProvider.signPsbt).toHaveBeenCalledWith(withdrawPsbt, { - contracts: [ - { - id: ContractId.WITHDRAW, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - timelockBlocks: stakingInput.stakingTimelock, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, - }); - expect(transaction.toHex()).toBe( - Psbt.fromHex(signedwithdrawPsbt).extractTransaction().toHex(), - ); - expect(fee).toEqual(516); - }); - }); - - // Slashed - describe("createSignedBtcWithdrawSlashingTransaction", () => { - it("should validate version params", async () => { - const version = 5; - - try { - await manager.createSignedBtcWithdrawSlashingTransaction( - stakerInfo, - stakingInput, - version, - slashingTx, - 2, - ); - } catch (e: any) { - expect(e.message).toMatch( - `Babylon params not found for version ${version}`, - ); - } - }); - - it("should create withdrawal tx", async () => { - const slashingPsbt = - "70736274ff01005e02000000019756644d209e088a6d0f29a20bd5bcc60496a6d2302df0e29019f3f4f572cc6101000000000500000001201e000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac42215c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac025200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b2c001172050929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; - const signedSlashingPsbt = - "70736274ff01005e02000000019756644d209e088a6d0f29a20bd5bcc60496a6d2302df0e29019f3f4f572cc6101000000000500000001201e000000000000225120f8115abbfc76b4c0d938c09511b76bfe43332b7126534f1082d693f419a9adce000000000001012b401f00000000000022512032ce4567cd1a74ae293fc51b5afbfd6b166051ab6aee1c6b9aacace60eeb5ac401088903402692ff631a6a6ce5dfdf33ed2d3e9573d7752cbc4ad98f8d117417c24d7b2a4f98458aa7c4e95f59bcf1c41b80e4a433867f3dd4540ad121f0a546b7945a5fec24200874876147fd7522d617e83bf845f7fb4981520e3c2f749ad4a2ca1bd660ef0cad55b221c150929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac00000"; - btcProvider.signPsbt.mockResolvedValueOnce(signedSlashingPsbt); - - const { transaction, fee } = - await manager.createSignedBtcWithdrawSlashingTransaction( - stakerInfo, - stakingInput, - version, - slashingTx, - 2, - ); - - expect(btcProvider.signPsbt).toHaveBeenCalledWith(slashingPsbt, { - contracts: [ - { - id: ContractId.WITHDRAW, - params: { - stakerPk: stakerInfo.publicKeyNoCoordHex, - timelockBlocks: params[version].unbondingTime, - }, - }, - ], - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, - }); - - expect(transaction.toHex()).toBe( - Psbt.fromHex(signedSlashingPsbt).extractTransaction().toHex(), - ); - expect(fee).toEqual(288); - }); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts deleted file mode 100644 index 9be9f6a20f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/createStakingTx.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { address, networks } from "bitcoinjs-lib"; -import { ObservableStaking, transactionIdToHash } from "../../../src"; -import * as observableStakingScriptData from "../../../src/staking/observable/observableStakingScript"; -import { ObservableVersionedStakingParams } from "../../../src/types/params"; -import { UTXO } from "../../../src/types/UTXO"; -import { StakingError, StakingErrorCode } from "../../../src/error"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import * as stakingUtils from "../../../src/utils/staking/validation"; -import * as staking from "../../../src/staking/transactions"; -import { ObservableStakingDatagen } from "../../helper/datagen/observable"; - -// TODO: To be removed -describe.each([networks.bitcoin, networks.testnet])("Observal - Create staking transaction", (network) => { - const dataGenerator= new ObservableStakingDatagen(network) - const networkName = network === networks.bitcoin ? "mainnet" : "testnet"; - - let stakerInfo: { address: string, publicKeyNoCoordHex: string, publicKeyWithCoord: string }; - let finalityProviderPksNoCoord: string[]; - let params: ObservableVersionedStakingParams; - let timelock: number; - let utxos: UTXO[]; - const feeRate = 1; - let observableStaking: ObservableStaking; - - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.restoreAllMocks(); - - const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); - const { address, scriptPubKey } = dataGenerator.getAddressAndScriptPubKey( - publicKey, - ).taproot; - stakerInfo = { - address, - publicKeyNoCoordHex: publicKeyNoCoord, - publicKeyWithCoord: publicKey, - }; - finalityProviderPksNoCoord = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(1); - params = dataGenerator.generateStakingParams(true); - timelock = dataGenerator.generateRandomTimelock(params); - utxos = dataGenerator.generateRandomUTXOs( - params.maxStakingAmountSat * dataGenerator.getRandomIntegerBetween(1, 100), - dataGenerator.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - observableStaking = new ObservableStaking( - network, stakerInfo, - params, finalityProviderPksNoCoord, timelock, - ); - }); - - it(`${networkName} should throw an error if input data validation failed`, async () => { - jest.spyOn(stakingUtils, "validateStakingTxInputData").mockImplementation(() => { - throw new StakingError(StakingErrorCode.INVALID_INPUT, "some error"); - }); - - expect(() => observableStaking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.INVALID_INPUT, "some error") - ); - }); - - it(`${networkName} should throw an error if fail to build scripts`, async () => { - jest.spyOn(observableStakingScriptData, "ObservableStakingScriptData").mockImplementation(() => { - throw new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error"); - }); - expect(() => observableStaking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.SCRIPT_FAILURE, "some error") - ); - }); - - it(`${networkName} should throw an error if fail to build staking tx`, async () => { - jest.spyOn(staking, "stakingTransaction").mockImplementation(() => { - throw new Error("fail to build staking tx"); - }); - - expect(() => observableStaking.createStakingTransaction( - params.minStakingAmountSat, - utxos, - feeRate, - )).toThrow( - new StakingError(StakingErrorCode.BUILD_TRANSACTION_FAILURE, "fail to build staking tx") - ); - }); - - it(`${networkName} should successfully create a observable staking transaction`, async () => { - const amount = dataGenerator.getRandomIntegerBetween( - params.minStakingAmountSat, params.maxStakingAmountSat, - ); - const { transaction, fee} = observableStaking.createStakingTransaction( - amount, - utxos, - feeRate, - ); - - expect(transaction).toBeDefined(); - expect(fee).toBeGreaterThan(0); - - const psbt = observableStaking.toStakingPsbt(transaction, utxos); - // Check the inputs - expect(transaction.ins.length).toBeGreaterThan(0); - - // Check the outputs - expect(transaction.outs.length).toBeGreaterThanOrEqual(1); - // build the psbt input amount from psbt.data.inputs - let psbtInputAmount = 0; - for (let i = 0; i < psbt.data.inputs.length; i++) { - const newValue = psbt.data.inputs[i].witnessUtxo?.value || 0; - psbtInputAmount += newValue; - } - const psbtChangeAmount = psbtInputAmount - amount - fee; - - let txInputAmount = 0; - for (let i = 0; i < transaction.ins.length; i++) { - const input = transaction.ins[i]; - const utxo = utxos.find(u => - transactionIdToHash(u.txid).toString("hex") === input.hash.toString("hex") - && u.vout === input.index, - ); - txInputAmount += utxo?.value || 0; - } - const changeAmount = txInputAmount - amount - fee; - expect(txInputAmount).toBeGreaterThanOrEqual(amount + fee); - if (changeAmount > BTC_DUST_SAT) { - expect(transaction.outs[transaction.outs.length - 1].value).toEqual(changeAmount); - expect(transaction.outs[transaction.outs.length - 1].script) - .toEqual(address.toOutputScript(stakerInfo.address, network)); - } - expect(transaction.outs[0].value).toEqual(amount); - expect(psbt.txOutputs[0].value).toEqual(amount); - - - // Check the psbt properties - expect(transaction.locktime).toBe(params.btcActivationHeight - 1); - expect(transaction.version).toBe(2); - transaction.ins.map((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - - // Check the data embed script(OP_RETURN) - const scripts = observableStaking.buildScripts(); - const dataEmbedOutput = transaction.outs.find((output) => - output.script.equals(scripts.dataEmbedScript), - ); - expect(dataEmbedOutput).toBeDefined(); - - expect(psbtChangeAmount).toEqual(changeAmount); - expect(psbtInputAmount).toEqual(txInputAmount); - // lock time and version are the same between psbt and transaction - expect(psbt.locktime).toEqual(transaction.locktime); - expect(psbt.version).toEqual(transaction.version); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts deleted file mode 100644 index daec543180..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/observableStakingScript.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { opcodes, script } from "bitcoinjs-lib"; -import { ObservableStakingScriptData } from "../../../src"; - -describe("observableStakingScript", () => { - const pk1 = Buffer.from( - "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd068", - "hex", - ); - const pk2 = Buffer.from( - "f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8e", - "hex", - ); - const pk3 = Buffer.from( - "17921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4", - "hex", - ); - const pk4 = Buffer.from( - "76d1ae01f8fb6bf30108731c884cddcf57ef6eef2d9d9559e130894e0e40c62c", - "hex", - ); - const pk5 = Buffer.from( - "49766ccd9e3cd94343e2040474a77fb37cdfd30530d05f9f1e96ae1e2102c86e", - "hex", - ); - const pk6 = Buffer.from( - "063deb187a4bf11c114cf825a4726e4c2c35fea5c4c44a20ff08a30a752ec7e0", - "hex", - ); - const invalidPk = Buffer.from( - "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd0", - "hex", - ); - const stakingTimeLock = 65535; - const unbondingTimeLock = 1000; - const magicBytes = Buffer.from("62626234", "hex"); - - describe("Error path", () => { - it("should throw if more than one finality providers when building data embed script", () => { - const script = new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2, pk6], // More than one FP Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - magicBytes - ); - expect(() => - script.buildDataEmbedScript() - ).toThrow("Only a single finality provider key is supported"); - }); - - it("should fail if the magic bytes are below 4 in length", () => { - expect( - () => - new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - Buffer.from("aaaaaa", "hex"), - ), - ).toThrow("Invalid script data provided"); - }); - it("should fail if the magic bytes are above 4 in length", () => { - expect( - () => - new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - Buffer.from("aaaaaaaaaa", "hex"), - ), - ).toThrow("Invalid script data provided"); - }); - }); - - describe("Happy path", () => { - it("should succeed with valid input data", () => { - const scriptData = new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - magicBytes, - ); - expect(scriptData).toBeInstanceOf(ObservableStakingScriptData); - }); - - it("should build valid data embed script", () => { - const scriptData = new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - magicBytes, - ); - const dataEmbedScript = scriptData.buildDataEmbedScript(); - const decompiled = script.decompile(dataEmbedScript); - expect(decompiled).toEqual([ - opcodes.OP_RETURN, - Buffer.concat([ - magicBytes, - Buffer.from([0]), // Version byte - pk1, - pk2, - Buffer.from([stakingTimeLock >> 8, stakingTimeLock & 0xff]), // Staking timelock in big endian - ]), - ]); - }); - - it("should build valid staking scripts", () => { - const scriptData = new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - magicBytes, - ); - const scripts = scriptData.buildScripts(); - expect(scripts).toHaveProperty("timelockScript"); - expect(scripts).toHaveProperty("unbondingScript"); - expect(scripts).toHaveProperty("slashingScript"); - expect(scripts).toHaveProperty("unbondingTimelockScript"); - expect(scripts).toHaveProperty("dataEmbedScript"); - }); - - it("should validate correctly with valid input data", () => { - const scriptData = new ObservableStakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - magicBytes, - ); - expect(scriptData.validate()).toBe(true); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts deleted file mode 100644 index b8a433c938..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/observable/validation.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { networks } from 'bitcoinjs-lib'; -import { ObservableStaking } from '../../../src/staking/observable'; -import { ObservableStakingDatagen } from '../../helper/datagen/observable'; - -// TODO: To be removed -describe.each([networks.bitcoin, networks.testnet])("Observable", (network) => { - const networkName = network === networks.bitcoin ? "mainnet" : "testnet"; - const dataGenerator = new ObservableStakingDatagen(network); - - describe(`${networkName} validateParams`, () => { - const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); - const { address } = dataGenerator.getAddressAndScriptPubKey( - publicKey, - ).taproot; - const params = dataGenerator.generateStakingParams(true); - - const stakerInfo = { - address, - publicKeyNoCoordHex: publicKeyNoCoord, - publicKeyWithCoord: publicKey, - }; - const validParams = dataGenerator.generateStakingParams(); - - const finalityProviderPksNoCoordHex = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(1); - - it('should pass with valid parameters', () => { - expect(() => new ObservableStaking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(params), - )).not.toThrow(); - }); - - it('should throw an error if no tag', () => { - const params = { ...validParams, tag: "" }; - - expect(() => new ObservableStaking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(params), - )).toThrow( - "Observable staking parameters must include tag" - ); - }); - - it('should throw an error if no btcActivationHeight', () => { - const params = { ...validParams, btcActivationHeight: 0 }; - - expect(() => new ObservableStaking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(params), - )).toThrow( - "Observable staking parameters must include a positive activation height" - ); - }); - - it('should throw an error if number of finality provider public keys is not 1', () => { - const finalityProviderPksNoCoordHex = dataGenerator.generateRandomFidelityProviderPksNoCoordHex(2); - - expect(() => new ObservableStaking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(params), - )).toThrow( - "Observable staking requires exactly one finality provider public key" - ); - }); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts deleted file mode 100644 index a6c525648f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingExpansionPsbt.test.ts +++ /dev/null @@ -1,464 +0,0 @@ -import { Transaction } from "bitcoinjs-lib"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { stakingExpansionTransaction } from "../../../src/staking/transactions"; -import { transactionIdToHash } from "../../../src/utils/btc"; -import { testingNetworks } from "../../helper"; -import { stakingExpansionPsbt } from "../../../src/staking/psbt"; -import { internalPubkey } from "../../../src/constants/internalPubkey"; - -describe.each(testingNetworks)("Transactions - ", ( - {network, networkName, datagen} -) => { - describe.each(Object.values(datagen))("stakingExpansionPsbt", ( - dataGenerator - ) => { - const feeRate = 1; - const stakerKeyPair = dataGenerator.generateRandomKeyPair(); - const { - stakingTx: previousStakingTx, - stakingAmountSat, - stakerInfo, - scriptPubKey, - stakingInstance: previousStakingInstance, - } = dataGenerator.generateRandomStakingTransaction( - network, - feeRate, - stakerKeyPair, - ); - - const previousStakingScript = previousStakingInstance.buildScripts(); - const utxos = dataGenerator.generateRandomUTXOs( - 10000, // Big enough to cover the fees - dataGenerator.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - - describe("Error path", () => { - it(`${networkName} - should throw an error if the public key is invalid`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - const invalidPublicKey = Buffer.from("invalidPublicKey", "hex"); - expect(() => - stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - invalidPublicKey, - ), - ).toThrow("Invalid public key"); - }); - - it(`${networkName} - should throw an error if previous staking output not found`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - expect(() => - stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 999, // Invalid output index - }, - utxos, - previousStakingScript, - ), - ).toThrow("Previous staking output not found"); - }); - - it(`${networkName} - should throw an error if previous staking output is not P2TR`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Create a modified previous staking transaction with non-P2TR output - const modifiedPreviousTx = Transaction.fromBuffer(previousStakingTx.toBuffer()); - const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - modifiedPreviousTx.outs[0] = { - script: Buffer.from(nativeSegwit.scriptPubKey, "hex"), - value: stakingAmountSat, - }; - - expect(() => - stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: modifiedPreviousTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ), - ).toThrow("Previous staking output script type is not P2TR"); - }); - - it(`${networkName} - should throw an error if staking expansion transaction doesn't have exactly 2 inputs`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Create a modified expansion transaction with only 1 input - const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); - modifiedExpansionTx.ins = [modifiedExpansionTx.ins[0]]; // Remove second input - - expect(() => - stakingExpansionPsbt( - network, - modifiedExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ), - ).toThrow("Staking expansion transaction must have exactly 2 inputs"); - }); - - it(`${networkName} - should throw an error if previous staking input hash doesn't match`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Create a modified expansion transaction with wrong hash - const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); - modifiedExpansionTx.ins[0].hash = Buffer.from("0".repeat(64), "hex"); - - expect(() => - stakingExpansionPsbt( - network, - modifiedExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ), - ).toThrow("Previous staking input hash does not match"); - }); - - it(`${networkName} - should throw an error if previous staking input index doesn't match`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Create a modified expansion transaction with wrong index - const modifiedExpansionTx = Transaction.fromBuffer(stakingExpansionTx.toBuffer()); - modifiedExpansionTx.ins[0].index = 999; - - expect(() => - stakingExpansionPsbt( - network, - modifiedExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ), - ).toThrow("Previous staking input index does not match"); - }); - - it(`${networkName} - should throw an error if input UTXO is not found`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Use different UTXOs that don't match the transaction inputs - const differentUtxos = dataGenerator.generateRandomUTXOs( - 10000, - dataGenerator.getRandomIntegerBetween(1, 10), - ); - - expect(() => - stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - differentUtxos, - previousStakingScript, - ), - ).toThrow(/Input UTXO not found for txid:/); - }); - - }); - - describe("Happy path", () => { - it(`${networkName} - should return a valid PSBT with correct inputs and outputs`, () => { - const { - transaction: stakingExpansionTx, - fundingUTXO, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - const psbt = stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), - ); - - expect(psbt).toBeDefined(); - - // Check PSBT properties - expect(psbt.version).toBe(2); - expect(psbt.locktime).toBe(0); - - // Check inputs - expect(psbt.txInputs.length).toBe(2); - - // First input (previous staking output) - expect(psbt.txInputs[0].hash).toEqual(previousStakingTx.getHash()); - expect(psbt.txInputs[0].index).toBe(0); - expect(psbt.txInputs[0].sequence).toBe(NON_RBF_SEQUENCE); - expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); - expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); - expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(stakingAmountSat); - expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( - previousStakingTx.outs[0].script, - ); - - // Second input (funding UTXO) - expect(psbt.txInputs[1].hash).toEqual(transactionIdToHash(fundingUTXO.txid)); - expect(psbt.txInputs[1].index).toBe(fundingUTXO.vout); - expect(psbt.txInputs[1].sequence).toBe(NON_RBF_SEQUENCE); - - // Check outputs - expect(psbt.txOutputs.length).toBe(stakingExpansionTx.outs.length); - stakingExpansionTx.outs.forEach((output, index) => { - expect(psbt.txOutputs[index].value).toEqual(output.value); - expect(psbt.txOutputs[index].script).toEqual(output.script); - }); - }); - - it(`${networkName} - should work without publicKeyNoCoord parameter`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - const psbt = stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ); - - expect(psbt).toBeDefined(); - expect(psbt.txInputs.length).toBe(2); - - // First input should still have internalPubkey - expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); - - // Second input should not have tapInternalKey when publicKeyNoCoord is not provided - expect(psbt.data.inputs[1].tapInternalKey).toBeUndefined(); - }); - - it(`${networkName} - should have tapInternalKey for second input when publicKeyNoCoord is provided`, () => { - const { taproot } = dataGenerator.getAddressAndScriptPubKey(stakerKeyPair.publicKey); - - // Generate UTXOs with P2TR scriptPubKey for Taproot - const taprootUtxos = dataGenerator.generateRandomUTXOs( - 10000000, // Big enough to cover the fees - dataGenerator.getRandomIntegerBetween(1, 10), - taproot.scriptPubKey, // Use P2TR scriptPubKey - ); - - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - taproot.address, - feeRate, - taprootUtxos, // Use the P2TR UTXOs - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - const psbt = stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - taprootUtxos, // Use the P2TR UTXOs - previousStakingScript, - Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), - ); - - expect(psbt).toBeDefined(); - expect(psbt.txInputs.length).toBe(2); - - // First input should have internalPubkey - expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); - - // Second input should have tapInternalKey when publicKeyNoCoord is provided - expect(psbt.data.inputs[1].tapInternalKey).toEqual( - Buffer.from(stakerKeyPair.publicKeyNoCoord, "hex"), - ); - }); - - it(`${networkName} - should preserve transaction version and locktime`, () => { - const { - transaction: stakingExpansionTx, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - feeRate, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ); - - // Modify version and locktime - stakingExpansionTx.version = 1; - stakingExpansionTx.locktime = 1000; - - const psbt = stakingExpansionPsbt( - network, - stakingExpansionTx, - { - stakingTx: previousStakingTx, - outputIndex: 0, - }, - utxos, - previousStakingScript, - ); - - expect(psbt.version).toBe(1); - expect(psbt.locktime).toBe(1000); - }); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts deleted file mode 100644 index cf751a0ee4..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/stakingPsbt.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { address, Psbt } from "bitcoinjs-lib"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { StakingScripts, stakingTransaction } from "../../../src/index"; -import { ObservableStakingScripts } from "../../../src/staking/observable"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; -import { stakingPsbt } from "../../../src/staking/psbt"; - -describe.each(testingNetworks)("Transactions - ", ( - {network, networkName, datagen} -) => { - describe.each(Object.values(datagen))("stakingPsbt", ( - dataGenerator - ) => { - const mockScripts = dataGenerator.generateMockStakingScripts(); - const feeRate = DEFAULT_TEST_FEE_RATE; - const randomAmount = Math.floor(Math.random() * 100000000) + 1000; - // Create enough utxos to cover the amount - const utxos = dataGenerator.generateRandomUTXOs( - randomAmount + 1000000, // let's give enough satoshis to cover the fee - Math.floor(Math.random() * 10) + 1, - ); - describe("Error path", () => { - const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ).taproot.address; - - it(`${networkName} - should throw an error if the public key is invalid`, () => { - const tx = stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - feeRate, - ); - const invalidPublicKey = Buffer.from("invalidPublicKey", "hex"); - expect(() => - stakingPsbt(tx.transaction, network, utxos, invalidPublicKey), - ).toThrow("Invalid public key"); - }); - - it(`${networkName} - should throw an error if the input utxos are not found`, () => { - const tx = stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - feeRate, - ); - const anotherUTXO = dataGenerator.generateRandomUTXOs( - randomAmount + 1000000, // let's give enough satoshis to cover the fee - Math.floor(Math.random() * 10) + 1, - ); - - expect(() => - stakingPsbt(tx.transaction, network, anotherUTXO), - ).toThrow(/Input UTXO not found for txid:/); - }); - }); - - describe("Happy path", () => { - const { taproot, nativeSegwit } = - dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - - it(`${networkName} - should return a valid psbt result with tapInternalKey`, () => { - let txResult = stakingTransaction( - mockScripts, - randomAmount, - taproot.address, - utxos, - network, - feeRate, - ); - - let psbtResult = stakingPsbt( - txResult.transaction, - network, - utxos, - Buffer.from( - dataGenerator.generateRandomKeyPair().publicKeyNoCoord, - "hex", - ), - ); - - validateCommonFields( - psbtResult, - randomAmount, - txResult.fee, - taproot.address, - mockScripts, - ); - - txResult = stakingTransaction( - mockScripts, - randomAmount, - nativeSegwit.address, - utxos, - network, - feeRate, - ); - - psbtResult = stakingPsbt( - txResult.transaction, - network, - utxos, - Buffer.from( - dataGenerator.generateRandomKeyPair().publicKeyNoCoord, - "hex", - ), - ); - - validateCommonFields( - psbtResult, - randomAmount, - txResult.fee, - nativeSegwit.address, - mockScripts, - ); - }); - }); - }); - - const validateCommonFields = ( - psbt: Psbt, - randomAmount: number, - estimatedFee: number, - changeAddress: string, - mockScripts: StakingScripts | ObservableStakingScripts, - ) => { - expect(psbt).toBeDefined(); - // make sure the input amount is greater than the output amount - const inputAmount = psbt.data.inputs.reduce( - (sum, input) => sum + input.witnessUtxo!.value || 0, - 0, - ); - - const outputAmount = psbt.txOutputs.reduce( - (sum, output) => sum + output.value, - 0, - ); - expect(inputAmount).toBeGreaterThan(outputAmount); - expect(inputAmount - outputAmount - estimatedFee).toBeLessThan(BTC_DUST_SAT); - // check the change amount is correct and send to the correct address - if (inputAmount - (randomAmount + estimatedFee) > BTC_DUST_SAT) { - const expectedChangeAmount = inputAmount - (randomAmount + estimatedFee); - const changeOutput = psbt.txOutputs.find( - (output) => output.value === expectedChangeAmount, - ); - expect(changeOutput).toBeDefined(); - // also make sure the change address is correct by look up the `address` - expect( - psbt.txOutputs.find( - (output) => output.script.toString('hex') === address.toOutputScript( - changeAddress, - network, - ).toString('hex'), - ), - ).toBeDefined(); - } - - // check data embed output added to the transaction if the dataEmbedScript is provided - if ((mockScripts as any).dataEmbedScript) { - expect( - psbt.txOutputs.find((output) => - output.script.equals((mockScripts as any).dataEmbedScript), - ), - ).toBeDefined(); - } - // Check the staking amount is correct - expect( - psbt.txOutputs.find((output) => output.value === randomAmount), - ).toBeDefined(); - - psbt.txInputs.map((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - expect(psbt.version).toBe(2); - }; -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts deleted file mode 100644 index f4191a46b2..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/psbt/unbondingPsbt.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { stakingTransaction, unbondingTransaction } from "../../../src/index"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; -import { unbondingPsbt } from "../../../src/staking/psbt"; -import { internalPubkey } from "../../../src/constants/internalPubkey"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { Transaction } from "bitcoinjs-lib"; - -describe.each(testingNetworks)("Transactions - ", ( - {network, networkName, datagen} -) => { - describe.each(Object.values(datagen))("unbondingPsbt", ( - dataGenerator - ) => { - const mockScripts = dataGenerator.generateMockStakingScripts(); - const feeRate = DEFAULT_TEST_FEE_RATE; - const params = dataGenerator.generateStakingParams(); - const randomAmount = Math.floor( - Math.random() * 100000000 - ) + 1000 + params.unbondingFeeSat + BTC_DUST_SAT; - // Create enough utxos to cover the amount - const utxos = dataGenerator.generateRandomUTXOs( - randomAmount + 1000000, // let's give enough satoshis to cover the fee - Math.floor(Math.random() * 10) + 1, - ); - const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ).taproot.address; - - const tx = stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - feeRate, - ); - const unbondingTx = unbondingTransaction( - mockScripts, - tx.transaction, - params.unbondingFeeSat, - network, - ); - - it(`${networkName} - should return a valid psbt result with correct inputs and outputs`, () => { - const psbt = unbondingPsbt( - mockScripts, - unbondingTx.transaction, - tx.transaction, - network, - ); - - expect(psbt).toBeDefined(); - - // Check the psbt inputs - expect(psbt.txInputs.length).toBe(1); - expect(psbt.txInputs[0].hash).toEqual(tx.transaction.getHash()); - expect(psbt.data.inputs[0].tapInternalKey).toEqual(internalPubkey); - expect(psbt.data.inputs[0].tapLeafScript?.length).toBe(1); - expect(psbt.data.inputs[0].witnessUtxo?.value).toEqual(randomAmount); - expect(psbt.data.inputs[0].witnessUtxo?.script).toEqual( - tx.transaction.outs[0].script, - ); - expect(psbt.txInputs[0].sequence).toEqual(NON_RBF_SEQUENCE); - expect(psbt.txInputs[0].index).toEqual(0); - // Check the psbt outputs - expect(psbt.txOutputs.length).toBe(1); - expect(psbt.txOutputs[0].value).toEqual(randomAmount - params.unbondingFeeSat); - - // Check the psbt properties - expect(psbt.locktime).toBe(0); - expect(psbt.version).toBe(2); - }); - - it(`${networkName} - should throw error if unbonding tx has more than one output`, () => { - // Create a copy of unbonding tx and add another output - const invalidUnbondingTx = Transaction.fromBuffer(unbondingTx.transaction.toBuffer()); - invalidUnbondingTx.addOutput( - tx.transaction.outs[0].script, - 1000 - ); - - expect(() => unbondingPsbt( - mockScripts, - invalidUnbondingTx, - tx.transaction, - network, - )).toThrow("Unbonding transaction must have exactly one output"); - }); - - it(`${networkName} - should throw error if unbonding tx has more than one input`, () => { - // Create a copy of unbonding tx and add another input - const invalidUnbondingTx = Transaction.fromBuffer(unbondingTx.transaction.toBuffer()); - invalidUnbondingTx.addInput( - tx.transaction.getHash(), - 1, - NON_RBF_SEQUENCE - ); - - expect(() => unbondingPsbt( - mockScripts, - invalidUnbondingTx, - tx.transaction, - network, - )).toThrow("Unbonding transaction must have exactly one input"); - }); - - it(`${networkName} - should throw error if unbonding output script does - not match the expected script`, () => { - const differentScripts = dataGenerator.generateMockStakingScripts(); - expect(() => unbondingPsbt( - differentScripts, - unbondingTx.transaction, - tx.transaction, - network, - )).toThrow("Unbonding output script does not match the expected script while building psbt"); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts deleted file mode 100644 index 7ee348fcd7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/stakingScript.test.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { opcodes, script } from "bitcoinjs-lib"; -import { StakingScriptData } from "../../src"; - -describe("stakingScript", () => { - const pk1 = Buffer.from( - "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd068", - "hex", - ); - const pk2 = Buffer.from( - "f5199efae3f28bb82476163a7e458c7ad445d9bffb0682d10d3bdb2cb41f8e8e", - "hex", - ); - const pk3 = Buffer.from( - "17921cf156ccb4e73d428f996ed11b245313e37e27c978ac4d2cc21eca4672e4", - "hex", - ); - const pk4 = Buffer.from( - "76d1ae01f8fb6bf30108731c884cddcf57ef6eef2d9d9559e130894e0e40c62c", - "hex", - ); - const pk5 = Buffer.from( - "49766ccd9e3cd94343e2040474a77fb37cdfd30530d05f9f1e96ae1e2102c86e", - "hex", - ); - const invalidPk = Buffer.from( - "6f13a6d104446520d1757caec13eaf6fbcf29f488c31e0107e7351d4994cd0", - "hex", - ); - const stakingTimeLock = 65535; - const unbondingTimeLock = 1000; - - describe("Error path", () => { - it("should fail if the staker key is not 32 bytes", () => { - expect( - () => - new StakingScriptData( - invalidPk, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if a finality provider key is not 32 bytes", () => { - expect(() => - new StakingScriptData( - pk1, // Staker Pk - [pk2, invalidPk], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ) - ).toThrow("Invalid script data provided"); - }); - - it("should fail if a covenant emulator key is not 32 bytes", () => { - expect(() => - new StakingScriptData( - pk1, // Staker Pk - [pk2, pk3], // Finality Provider Pks - [pk4, invalidPk, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ) - ).toThrow("Invalid script data provided"); - }); - - it("should fail if the covenant emulators threshold is 0", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 0, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Missing required input values"); - }); - - it("should fail if the covenant emulators threshold is larger than the covenant emulators", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 4, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if the staking timelock is 0", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - 0, - unbondingTimeLock, - ), - ).toThrow("Missing required input values"); - }); - - it("should fail if the staking timelock is above the maximum", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - 65536, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if the unbonding timelock is 0", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - 0, - ), - ).toThrow("Missing required input values"); - }); - - it("should fail if the unbonding timelock is above the maximum", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - 65536, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if the staker pk is in the finality providers list", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2, pk1], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if the staker pk is in the covenants list", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk1, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if a finality provider pk is in the covenants list", () => { - expect( - () => - new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk2, pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - - it("should fail if finality provider have duplicate keys", () => { - expect( - () => - new StakingScriptData( - pk1, - [pk2, pk2], - [pk3, pk4, pk5], - 2, - stakingTimeLock, - unbondingTimeLock, - ), - ).toThrow("Invalid script data provided"); - }); - }); - - describe("Happy path", () => { - it("should succeed with valid input data", () => { - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - expect(scriptData).toBeInstanceOf(StakingScriptData); - }); - - it("should build valid staking timelock script", () => { - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - const timelockScript = scriptData.buildStakingTimelockScript(); - const decompiled = script.decompile(timelockScript); - expect(decompiled).toEqual([ - pk1, - opcodes.OP_CHECKSIGVERIFY, - script.number.encode(stakingTimeLock), - opcodes.OP_CHECKSEQUENCEVERIFY, - ]); - }); - - it("should build valid unbonding timelock script", () => { - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - const unbondingTimelockScript = - scriptData.buildUnbondingTimelockScript(); - const decompiled = script.decompile(unbondingTimelockScript); - expect(decompiled).toEqual([ - pk1, - opcodes.OP_CHECKSIGVERIFY, - script.number.encode(unbondingTimeLock), - opcodes.OP_CHECKSEQUENCEVERIFY, - ]); - }); - - it("should build valid unbonding script", () => { - const pks = [pk3, pk4, pk5]; - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - pks, // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - - const sortedPks = [...pks].sort(Buffer.compare); - - const unbondingScript = scriptData.buildUnbondingScript(); - const decompiled = script.decompile(unbondingScript); - - const expectedScript = script.decompile( - Buffer.concat([ - script.compile([pk1, opcodes.OP_CHECKSIGVERIFY]), - script.compile([ - sortedPks[0], - opcodes.OP_CHECKSIG, - sortedPks[1], - opcodes.OP_CHECKSIGADD, - sortedPks[2], - opcodes.OP_CHECKSIGADD, - script.number.encode(2), - opcodes.OP_NUMEQUAL, - ]), - ]), - ); - - expect(decompiled).toEqual(expectedScript); - }); - - it("should build valid slashing script", () => { - const pks = [pk3, pk4, pk5]; - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - pks, // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - - const sortedPks = [...pks].sort(Buffer.compare); - - const slashingScript = scriptData.buildSlashingScript(); - const decompiled = script.decompile(slashingScript); - - const expectedScript = script.decompile( - Buffer.concat([ - script.compile([pk1, opcodes.OP_CHECKSIGVERIFY]), - script.compile([pk2, opcodes.OP_CHECKSIGVERIFY]), - script.compile([ - sortedPks[0], - opcodes.OP_CHECKSIG, - sortedPks[1], - opcodes.OP_CHECKSIGADD, - sortedPks[2], - opcodes.OP_CHECKSIGADD, - script.number.encode(2), - opcodes.OP_NUMEQUAL, - ]), - ]), - ); - - expect(decompiled).toEqual(expectedScript); - }); - - it("should build valid staking scripts", () => { - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - const scripts = scriptData.buildScripts(); - expect(scripts).toHaveProperty("timelockScript"); - expect(scripts).toHaveProperty("unbondingScript"); - expect(scripts).toHaveProperty("slashingScript"); - expect(scripts).toHaveProperty("unbondingTimelockScript"); - // We don't expect the data embed script to be present - expect(scripts).not.toHaveProperty("dataEmbedScript"); - }); - - it("should validate correctly with valid input data", () => { - const scriptData = new StakingScriptData( - pk1, // Staker Pk - [pk2], // Finality Provider Pks - [pk3, pk4, pk5], // covenant Pks - 2, - stakingTimeLock, - unbondingTimeLock, - ); - expect(scriptData.validate()).toBe(true); - }); - - it("should validate correctly with minimum valid staking and unbonding timelock", () => { - const scriptData = new StakingScriptData( - pk1, - [pk2], - [pk3, pk4, pk5], - 2, - 1, // Minimum valid staking timelock - 1, // Minimum valid unbonding timelock - ); - expect(scriptData.validate()).toBe(true); - }); - - it("should validate correctly with unique keys", () => { - const scriptData = new StakingScriptData( - pk1, - [pk2], - [pk3, pk4, pk5], - 2, - stakingTimeLock, - unbondingTimeLock, - ); - expect(scriptData.validate()).toBe(true); - }); - - it("should handle maximum valid staking and unbonding timelock", () => { - const scriptData = new StakingScriptData( - pk1, - [pk2], - [pk3, pk4, pk5], - 2, - 65535, // Maximum valid staking timelock - 65535, // Maximum valid unbonding timelock - ); - expect(scriptData.validate()).toBe(true); - }); - - it("should not fail for more than 1 finality provider", () => { - const scriptData = new StakingScriptData( - pk1, - [pk2, pk3], - [pk4, pk5], - 2, - stakingTimeLock, - unbondingTimeLock, - ); - expect(scriptData.validate()).toBe(true); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts deleted file mode 100644 index d6f31e875f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/slashingTransaction.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { payments } from "bitcoinjs-lib"; -import { - slashEarlyUnbondedTransaction, - slashTimelockUnbondedTransaction, - unbondingTransaction, -} from "../../../src"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { internalPubkey } from "../../../src/constants/internalPubkey"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; -import { NON_RBF_SEQUENCE, TRANSACTION_VERSION } from "../../../src/constants/psbt"; -import { getRandomPaymentScriptHex } from "../../helper/datagen/base"; - -describe.each(testingNetworks)("Transactions - ", ( - {network, networkName, datagen} -) => { - describe.each(Object.values(datagen))("slashingTransaction - ", ( - dataGenerator - ) => { - const stakerKeyPair = dataGenerator.generateRandomKeyPair(); - const stakingScripts = - dataGenerator.generateMockStakingScripts(stakerKeyPair); - const { stakingTx, stakingAmountSat} = dataGenerator.generateRandomStakingTransaction( - network, - DEFAULT_TEST_FEE_RATE, - stakerKeyPair, - ); - const slashingRate = dataGenerator.generateRandomSlashingRate(); - const slashingAmount = Math.round(stakingAmountSat * slashingRate); - const minSlashingFee = dataGenerator.getRandomIntegerBetween( - 1, - stakingAmountSat - slashingAmount - BTC_DUST_SAT - 1, - ); - const defaultOutputIndex = 0; - const slashingPkScriptHex = getRandomPaymentScriptHex( - dataGenerator.generateRandomKeyPair().publicKey, - ); - - describe(`${networkName} - slashTimelockUnbondedTransaction`, () => { - it("should throw an error if the slashing rate is not between 0 and 1", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - 0, - minSlashingFee, - network, - defaultOutputIndex, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - -0.1, - minSlashingFee, - network, - defaultOutputIndex, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - 1, - minSlashingFee, - network, - defaultOutputIndex, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - 1.1, - minSlashingFee, - network, - defaultOutputIndex, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - }); - - it("should throw an error if minimum slashing fee is less than 0", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - 0, - network, - defaultOutputIndex, - ), - ).toThrow("Minimum fee must be a positve integer"); - }); - - it("should throw an error if minimum slashing fee is not integer", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - 1.2, - network, - defaultOutputIndex, - ), - ).toThrow("Minimum fee must be a positve integer"); - }); - - it("should throw an error if the output index is less than 0", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - -1, - ), - ).toThrow("Output index must be an integer bigger or equal to 0"); - }); - - it("should throw an error if the output index is not integer", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - 1.2, - ), - ).toThrow("Output index must be an integer bigger or equal to 0"); - - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - 0.5, - ), - ).toThrow("Output index must be an integer bigger or equal to 0"); - }); - - it("should throw an error if the output index is greater than the number of outputs", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - stakingTx.outs.length, - ), - ).toThrow("Output index is out of range"); - }); - - it("should throw error if user funds after slashing and fees is less than dust", () => { - expect(() => - slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - Math.ceil(stakingAmountSat * (1 - slashingRate) + 1), - network, - 0, - ), - ).toThrow("User funds are less than dust limit"); - }); - - it("should create the slashing time lock unbonded tx psbt successfully", () => { - const { psbt } = slashTimelockUnbondedTransaction( - stakingScripts, - stakingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - 0, - ); - - expect(psbt).toBeDefined(); - expect(psbt.txOutputs.length).toBe(2); - // first output shall send slashed amount to the slashing script - expect(Buffer.from(psbt.txOutputs[0].script).toString("hex")).toBe(slashingPkScriptHex); - expect(psbt.txOutputs[0].value).toBe( - Math.round(stakingAmountSat * slashingRate), - ); - - // second output is the change output which send to unbonding timelock script address - const changeOutput = payments.p2tr({ - internalPubkey, - scriptTree: { output: stakingScripts.unbondingTimelockScript }, - network, - }); - expect(psbt.txOutputs[1].address).toBe(changeOutput.address); - const expectedChangeOutputValue = - stakingAmountSat - - Math.round(stakingAmountSat * slashingRate) - - minSlashingFee; - expect(psbt.txOutputs[1].value).toBe(expectedChangeOutputValue); - }); - }); - - describe(`${networkName} slashEarlyUnbondedTransaction - `, () => { - const { transaction: unbondingTx } = unbondingTransaction( - stakingScripts, - stakingTx, - 1, - network, - ); - - it("should throw an error if the slashing rate is not between 0 and 1", () => { - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - 0, - minSlashingFee, - network, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - -0.1, - minSlashingFee, - network, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - 1, - minSlashingFee, - network, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - 1.1, - minSlashingFee, - network, - ), - ).toThrow("Slashing rate must be between 0 and 1"); - }); - - it("should throw an error if minimum slashing fee is less than 0", () => { - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - slashingRate, - 0, - network, - ), - ).toThrow("Minimum fee must be a positve integer"); - }); - - it("should throw error if user funds is less than dust", () => { - const { transaction: unbondingTxWithLimitedAmount } = unbondingTransaction( - stakingScripts, - stakingTx, - 1, - network, - ); - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTxWithLimitedAmount, - slashingPkScriptHex, - slashingRate, - Math.ceil(stakingAmountSat * (1 - slashingRate) + 1), - network, - ), - ).toThrow("User funds are less than dust limit"); - }); - - it("should throw if its slashing amount is less than dust", () => { - const smallSlashingRate = BTC_DUST_SAT / stakingAmountSat; - expect(() => - slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - smallSlashingRate, - minSlashingFee, - network, - ) - ).toThrow("Slashing amount is less than dust limit"); - }); - - it("should create the slashing time lock unbonded tx psbt successfully", () => { - const { psbt } = slashEarlyUnbondedTransaction( - stakingScripts, - unbondingTx, - slashingPkScriptHex, - slashingRate, - minSlashingFee, - network, - ); - - const unbondingTxOutputValue = unbondingTx.outs[0].value; - - expect(psbt).toBeDefined(); - expect(psbt.txOutputs.length).toBe(2); - // first output shall send slashed amount to the slashing pk script (i.e burn output) - expect(Buffer.from(psbt.txOutputs[0].script).toString("hex")).toBe(slashingPkScriptHex); - expect(psbt.txOutputs[0].value).toBe( - Math.round(unbondingTxOutputValue * slashingRate), - ); - - // second output is the change output which send to unbonding timelock script address - const changeOutput = payments.p2tr({ - internalPubkey, - scriptTree: { output: stakingScripts.unbondingTimelockScript }, - network, - }); - expect(psbt.txOutputs[1].address).toBe(changeOutput.address); - const expectedChangeOutputValue = - unbondingTxOutputValue - - Math.round(unbondingTxOutputValue * slashingRate) - - minSlashingFee; - expect(psbt.txOutputs[1].value).toBe(expectedChangeOutputValue); - - expect(psbt.version).toBe(TRANSACTION_VERSION); - expect(psbt.locktime).toBe(0); - psbt.txInputs.forEach((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - }); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts deleted file mode 100644 index 128d87010c..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingExpansionTransaction.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { stakingExpansionTransaction } from "../../../src/staking/transactions"; -import { transactionIdToHash } from "../../../src/utils/btc"; -import { testingNetworks } from "../../helper"; - -describe("stakingExpansionTransaction", () => { - const [mainnet] = testingNetworks; - const { datagen: { stakingDatagen }, network } = mainnet; - const stakerKeyPair = stakingDatagen.generateRandomKeyPair(); - const { - stakingTx: previousStakingTx, - stakingAmountSat, - stakerInfo, - scriptPubKey, - stakingInstance: previousStakingInstance, - } = stakingDatagen.generateRandomStakingTransaction( - network, - 1, - stakerKeyPair, - ); - - const previousStakingScript = previousStakingInstance.buildScripts(); - const utxos = stakingDatagen.generateRandomUTXOs( - 10000, // Big enough to cover the fees - stakingDatagen.getRandomIntegerBetween(1, 10), - scriptPubKey, - ); - - it("should successfully expand a staking transaction", () => { - const { - transaction: stakingExpansionTx, - fee: stakingExpansionTxFee, - fundingUTXO, - } = stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - 1, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - expect(stakingExpansionTx).toBeDefined(); - expect(stakingExpansionTxFee).toBeGreaterThan(0); - // Must have two inputs: - // 1. The previous staking transaction output - // 2. The funding UTXO - expect(stakingExpansionTx.ins.length).toBe(2); - // First output must be the previous staking transaction output - expect(stakingExpansionTx.ins[0].hash).toEqual(previousStakingTx.getHash()); - // First output amount must match the previous staking amount - - // Must have more than or equal to 1 output - expect(stakingExpansionTx.outs.length).toBeGreaterThanOrEqual(1); - // Must match the same staking amount as previous staking transaction - expect(stakingExpansionTx.outs[0].value).toBe(stakingAmountSat); - - // Find the matching UTXO from the inputUTXOs list so that we know the amount. - const fundingUtxo = utxos.find( - (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) - ); - expect(fundingUtxo).toBeDefined(); - if (fundingUtxo!.value - stakingExpansionTxFee > BTC_DUST_SAT) { - // Must have a change output as the last output - expect(stakingExpansionTx.outs[ - stakingExpansionTx.outs.length - 1 - ].value).toBe(fundingUtxo!.value - stakingExpansionTxFee); - } - - // Both inputs should have the same sequence number (non-RBF) - expect(stakingExpansionTx.ins[0].sequence).toBe(NON_RBF_SEQUENCE); - expect(stakingExpansionTx.ins[1].sequence).toBe(NON_RBF_SEQUENCE); - // Should use standard transaction version - expect(stakingExpansionTx.version).toBe(2); - - expect(fundingUTXO).toBeDefined(); - expect(fundingUTXO.value).toBeGreaterThan(0); - - // Funding UTXO should be the same as the selected UTXO - expect(fundingUTXO.txid).toEqual(utxos.find( - (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) - )!.txid); - expect(fundingUTXO.value).toEqual(utxos.find( - (utxo) => transactionIdToHash(utxo.txid).equals(stakingExpansionTx.ins[1].hash) - )!.value); - }); - - it("should throw error when amount is less than or equal to 0", () => { - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - 0, // Invalid amount - stakerInfo.address, - 1, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Amount and fee rate must be bigger than 0"); - }); - - it("should throw error when fee rate is less than or equal to 0", () => { - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - 0, // Invalid fee rate - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Amount and fee rate must be bigger than 0"); - }); - - it("should throw error when change address is invalid", () => { - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - "invalid_address", // Invalid address - 1, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Invalid BTC change address"); - }); - - it("should throw error when expansion amount does not match previous staking amount", () => { - const differentAmount = stakingAmountSat + 1000; // Different amount - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - differentAmount, - stakerInfo.address, - 1, - utxos, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Expansion staking transaction amount must be equal to the previous staking amount"); - }); - - it("should throw error when no UTXOs are available for funding", () => { - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - 1, - [], // Empty UTXOs - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Insufficient funds"); - }); - - it("should throw error when no UTXOs can cover the required fees", () => { - // Create a UTXO with a value less than the fees - const smallUtxo = stakingDatagen.generateRandomUTXOs( - 100, // Much less than the fees - 1, - scriptPubKey, - ); - expect(() => - stakingExpansionTransaction( - network, - previousStakingScript, - stakingAmountSat, - stakerInfo.address, - 1, - smallUtxo, - { - stakingTx: previousStakingTx, - scripts: previousStakingScript, - }, - ) - ).toThrow("Insufficient funds: unable to find a UTXO to cover the fees"); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts deleted file mode 100644 index 04989d88b4..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/stakingTransaction.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { address } from "bitcoinjs-lib"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { NON_RBF_SEQUENCE } from "../../../src/constants/psbt"; -import { StakingScripts, stakingTransaction, transactionIdToHash, UTXO } from "../../../src/index"; -import { ObservableStakingScripts } from "../../../src/staking/observable"; -import { TransactionResult } from "../../../src/types/transaction"; -import { getStakingTxInputUTXOsAndFees } from "../../../src/utils/fee"; -import { buildStakingTransactionOutputs } from "../../../src/utils/staking"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; - -describe("StakingTransaction - Cross env error", () => { - const [mainnet, testnet] = testingNetworks; - const envPairs = [ - { - mainnetDataGenerator: mainnet.datagen.stakingDatagen, - testnetDataGenerator: testnet.datagen.stakingDatagen, - }, - ]; - - envPairs.map(({ mainnetDataGenerator, testnetDataGenerator }) => { - const randomAmount = Math.floor(Math.random() * 100000000) + 1000; - - it("should throw an error if the testnet inputs are used on mainnet", () => { - const randomChangeAddress = - testnetDataGenerator.getAddressAndScriptPubKey( - mainnetDataGenerator.generateRandomKeyPair().publicKey, - ).nativeSegwit.address; - const utxos = testnetDataGenerator.generateRandomUTXOs( - randomAmount + 1000000, - Math.floor(Math.random() * 10) + 1, - ); - expect(() => - stakingTransaction( - testnetDataGenerator.generateMockStakingScripts(), - randomAmount, - randomChangeAddress, - utxos, - mainnet.network, - 1, - ), - ).toThrow("Invalid change address"); - }); - - it("should throw an error if the mainnet inputs are used on testnet", () => { - const randomChangeAddress = - mainnetDataGenerator.getAddressAndScriptPubKey( - mainnetDataGenerator.generateRandomKeyPair().publicKey, - ).nativeSegwit.address; - const utxos = mainnetDataGenerator.generateRandomUTXOs( - randomAmount + 1000000, - Math.floor(Math.random() * 10) + 1, - ); - expect(() => - stakingTransaction( - mainnetDataGenerator.generateMockStakingScripts(), - randomAmount, - randomChangeAddress, - utxos, - testnet.network, - 1, - ), - ).toThrow("Invalid change address"); - }); - }); -}); - -describe.each(testingNetworks)("Transactions - ", ( - {network, networkName, datagen} -) => { - describe.each(Object.values(datagen))("stakingTransaction", ( - dataGenerator - ) => { - const mockScripts = dataGenerator.generateMockStakingScripts(); - const feeRate = DEFAULT_TEST_FEE_RATE; - const randomAmount = Math.floor(Math.random() * 100000000) + 1000; - // Create enough utxos to cover the amount - const utxos = dataGenerator.generateRandomUTXOs( - randomAmount + 1000000, // let's give enough satoshis to cover the fee - Math.floor(Math.random() * 10) + 1, - ); - describe("Error path", () => { - const randomChangeAddress = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ).taproot.address; - - it(`${networkName} - should throw an error if the change address is invalid`, () => { - const validAddress = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ).taproot.address; - const invalidCharInAddress = validAddress.replace(validAddress[0], "I"); // I is an invalid character in base58 - const invalidAddressLegnth = validAddress.slice(0, -1); - const invalidAddresses = [ - "", - " ", - "banana", - invalidCharInAddress, - invalidAddressLegnth, - ]; - invalidAddresses.map((a) => { - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - a, // Invalid address - utxos, - network, - feeRate, - ), - ).toThrow("Invalid change address"); - }); - }); - - it(`${networkName} - should throw an error if the utxo value is too low`, () => { - // generate a UTXO that is too small to cover the fee - const scriptPubKey = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ).taproot.scriptPubKey; - const utxo = { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: scriptPubKey, - value: 1, - }; - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - [utxo], - network, - 1, - ), - ).toThrow( - "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees", - ); - }); - - it(`${networkName} - should ignore the invalid utxo if the utxo scriptPubKey is invalid`, () => { - const utxo = { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: `abc${dataGenerator.generateRandomKeyPair().publicKey}`, // this is not a valid scriptPubKey - value: 10000000000000, - }; - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - [utxo], - network, - 1, - ), - ).toThrow("Insufficient funds: no valid UTXOs available for staking") - }); - - it(`${networkName} - should throw an error if UTXO is empty`, () => { - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - [], - network, - 1, - ), - ).toThrow("Insufficient funds"); - }); - - it(`${networkName} - should throw an error if the lock height is invalid`, () => { - // 500000000 is the maximum lock height in btc - const invalidLockHeight = 500000000 + 1; - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - feeRate, - invalidLockHeight, - ), - ).toThrow("Invalid lock height"); - }); - - it(`${networkName} - should throw an error if the amount is less than or equal to 0`, () => { - // Test case: amount is 0 - expect(() => - stakingTransaction( - mockScripts, - 0, // Invalid amount - randomChangeAddress, - utxos, - network, - dataGenerator.generateRandomFeeRates(), // Valid fee rate - ), - ).toThrow("Amount and fee rate must be bigger than 0"); - - // Test case: amount is -1 - expect(() => - stakingTransaction( - mockScripts, - -1, // Invalid amount - randomChangeAddress, - utxos, - network, - dataGenerator.generateRandomFeeRates(), // Valid fee rate - ), - ).toThrow("Amount and fee rate must be bigger than 0"); - }); - - it("should throw an error if the fee rate is less than or equal to 0", () => { - // Test case: fee rate is 0 - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - 0, // Invalid fee rate - ), - ).toThrow("Amount and fee rate must be bigger than 0"); - - // Test case: fee rate is -1 - expect(() => - stakingTransaction( - mockScripts, - randomAmount, - randomChangeAddress, - utxos, - network, - -1, // Invalid fee rate - ), - ).toThrow("Amount and fee rate must be bigger than 0"); - }); - }); - - describe("Happy path", () => { - // build the outputs - const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); - // A rough estimating of the fee, the end result should not be too far from this - const { fee: estimatedFee } = getStakingTxInputUTXOsAndFees( - utxos, - randomAmount, - feeRate, - outputs, - ); - const { taproot, nativeSegwit } = - dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - - it(`${networkName} - should return a valid transaction result`, () => { - const transactionResultTaproot = stakingTransaction( - mockScripts, - randomAmount, - taproot.address, - utxos, - network, - feeRate, - ); - validateCommonFields( - transactionResultTaproot, - utxos, - randomAmount, - estimatedFee, - taproot.address, - mockScripts, - ); - - const transactionResultNativeSegwit = stakingTransaction( - mockScripts, - randomAmount, - nativeSegwit.address, - utxos, - network, - feeRate, - ); - validateCommonFields( - transactionResultNativeSegwit, - utxos, - randomAmount, - estimatedFee, - nativeSegwit.address, - mockScripts, - ); - }); - - it(`${networkName} - should return a valid transaction result with lock field`, () => { - const lockHeight = Math.floor(Math.random() * 1000000) + 100; - const transactionResult = stakingTransaction( - mockScripts, - randomAmount, - taproot.address, - utxos, - network, - feeRate, - lockHeight, - ); - validateCommonFields( - transactionResult, - utxos, - randomAmount, - estimatedFee, - taproot.address, - mockScripts, - ); - // check the lock height is correct - expect(transactionResult.transaction.locktime).toEqual(lockHeight); - }); - }); - }); - - const validateCommonFields = ( - transactionResult: TransactionResult, - utxos: UTXO[], - randomAmount: number, - estimatedFee: number, - changeAddress: string, - mockScripts: StakingScripts | ObservableStakingScripts, - ) => { - expect(transactionResult).toBeDefined(); - // expect the estimated fee and the actual fee is the same - expect(transactionResult.fee).toBe(estimatedFee); - // make sure the input amount is greater than the output amount - const { transaction, fee } = transactionResult; - const inputAmount = transaction.ins.reduce( - (sum, input) => { - const id = input.hash.toString("hex"); - const utxo = utxos.find((utxo) => - transactionIdToHash(utxo.txid).toString("hex") === id - && utxo.vout === input.index, - ); - return sum + utxo!.value; - }, - 0, - ); - const outputAmount = transaction.outs.reduce( - (sum, output) => sum + output.value, - 0, - ); - expect(inputAmount).toBeGreaterThan(outputAmount); - expect(inputAmount - outputAmount - fee).toBeLessThan(BTC_DUST_SAT); - // check the change amount is correct and send to the correct address - if (inputAmount - (randomAmount + fee) > BTC_DUST_SAT) { - const expectedChangeAmount = inputAmount - (randomAmount + fee); - const changeOutput = transaction.outs.find( - (output) => output.value === expectedChangeAmount, - ); - expect(changeOutput).toBeDefined(); - // also make sure the change address is correct by look up the `address` - expect( - transaction.outs.find( - (output) => output.script.toString('hex') === address.toOutputScript( - changeAddress, - network, - ).toString('hex'), - ), - ).toBeDefined(); - } - - // check data embed output added to the transaction if the dataEmbedScript is provided - if ((mockScripts as any).dataEmbedScript) { - expect( - transaction.outs.find((output) => - output.script.equals((mockScripts as any).dataEmbedScript), - ), - ).toBeDefined(); - } - // Check the staking amount is correct - expect( - transaction.outs.find((output) => output.value === randomAmount), - ).toBeDefined(); - - transaction.ins.map((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - expect(transaction.version).toBe(2); - }; -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts deleted file mode 100644 index cb13a7fbe6..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/unbondingTransaction.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { unbondingTransaction } from "../../../src"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { - NON_RBF_SEQUENCE, - TRANSACTION_VERSION, -} from "../../../src/constants/psbt"; -import { testingNetworks } from "../../helper"; - -describe.each(testingNetworks)( - "Transactions - ", - ({ networkName, network, datagen }) => { - describe.each(Object.values(datagen))( - "unbondingTransaction", - (dataGenerator) => { - const stakerKeyPair = dataGenerator.generateRandomKeyPair(); - const { stakingTx, stakingAmountSat } = - dataGenerator.generateRandomStakingTransaction( - network, - 1, - stakerKeyPair, - ); - const stakingScripts = - dataGenerator.generateMockStakingScripts(stakerKeyPair); - - describe(`${networkName} - `, () => { - it("should throw an error if the unbonding fee is not postive number", () => { - expect(() => - unbondingTransaction(stakingScripts, stakingTx, 0, network), - ).toThrow("Unbonding fee must be bigger than 0"); - }); - - it("should throw if output index is negative", () => { - expect(() => - unbondingTransaction( - stakingScripts, - stakingTx, - dataGenerator.getRandomIntegerBetween(1, 10000), - network, - -1, - ), - ).toThrow("Output index must be bigger or equal to 0"); - }); - - it("should throw if output is less than dust limit", () => { - const unbondingFee = stakingAmountSat - BTC_DUST_SAT + 1; - expect(() => - unbondingTransaction( - stakingScripts, - stakingTx, - unbondingFee, - network, - 0, - ), - ).toThrow("Output value is less than dust limit"); - }); - - it("should return psbt for unbonding transaction", () => { - const unbondingFee = dataGenerator.getRandomIntegerBetween( - 1, - stakingAmountSat - BTC_DUST_SAT - 1, - ); - const { transaction } = unbondingTransaction( - stakingScripts, - stakingTx, - unbondingFee, - network, - 0, - ); - expect(transaction).toBeDefined(); - expect(transaction.outs.length).toBe(1); - // check output value - expect(transaction.outs[0].value).toBe( - stakingAmountSat - unbondingFee, - ); - - expect(transaction.locktime).toBe(0); - expect(transaction.version).toBe(TRANSACTION_VERSION); - transaction.ins.forEach((input) => { - expect(input.sequence).toBe(NON_RBF_SEQUENCE); - }); - }); - }); - }, - ); - }, -); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts deleted file mode 100644 index e732d7e496..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/transactions/withdrawTransaction.test.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { Network, Transaction, script } from "bitcoinjs-lib"; -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { TRANSACTION_VERSION } from "../../../src/constants/psbt"; -import { - StakingParams, - StakingScripts, - withdrawEarlyUnbondedTransaction, - withdrawSlashingTransaction, - withdrawTimelockUnbondedTransaction, -} from "../../../src/index"; -import { StakerInfo } from "../../../src/staking"; -import { PsbtResult } from "../../../src/types/transaction"; -import { getWithdrawTxFee } from "../../../src/utils/fee"; -import { - deriveSlashingOutput, - findMatchingTxOutputIndex, -} from "../../../src/utils/staking"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; -import { - KeyPair, - SlashingType, - StakingDataGenerator, -} from "../../helper/datagen/base"; -import { ObservableStakingDatagen } from "../../helper/datagen/observable"; - -interface WithdrawTransactionTestData { - keyPair: KeyPair; - stakerInfo: StakerInfo; - stakingScripts: StakingScripts; - stakingTx: Transaction; - stakingAmountSat: number; - params: StakingParams; -} - -const setupTestData = ( - network: Network, - dataGenerator: ObservableStakingDatagen | StakingDataGenerator, -): WithdrawTransactionTestData => { - const stakerKeyPair = dataGenerator.generateRandomKeyPair(); - - const stakingScripts = - dataGenerator.generateMockStakingScripts(stakerKeyPair); - const { stakingTx, stakerInfo, params, stakingAmountSat } = - dataGenerator.generateRandomStakingTransaction(network, 1, stakerKeyPair); - - return { - keyPair: stakerKeyPair, - stakerInfo, - stakingScripts, - stakingTx, - stakingAmountSat, - params, - }; -}; - -describe.each(testingNetworks)( - "withdrawTransaction", - ({ networkName, network, datagen }) => { - describe.each(Object.values(datagen))( - "withdrawTransaction", - (dataGenerator) => { - let testData: WithdrawTransactionTestData; - - beforeEach(() => { - jest.restoreAllMocks(); - testData = setupTestData(network, dataGenerator); - }); - - it(`${networkName} - should throw an error if the fee rate is less than or equal to 0`, () => { - expect(() => - withdrawEarlyUnbondedTransaction( - { - unbondingTimelockScript: - testData.stakingScripts.unbondingTimelockScript, - slashingScript: testData.stakingScripts.slashingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - 0, - ), - ).toThrow("Withdrawal feeRate must be bigger than 0"); - - expect(() => - withdrawTimelockUnbondedTransaction( - { - timelockScript: testData.stakingScripts.timelockScript, - slashingScript: testData.stakingScripts.slashingScript, - unbondingScript: testData.stakingScripts.unbondingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - 0, - ), - ).toThrow("Withdrawal feeRate must be bigger than 0"); - - expect(() => - withdrawEarlyUnbondedTransaction( - { - unbondingTimelockScript: - testData.stakingScripts.unbondingTimelockScript, - slashingScript: testData.stakingScripts.slashingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - -1, - ), - ).toThrow("Withdrawal feeRate must be bigger than 0"); - - expect(() => - withdrawTimelockUnbondedTransaction( - { - timelockScript: testData.stakingScripts.timelockScript, - slashingScript: testData.stakingScripts.slashingScript, - unbondingScript: testData.stakingScripts.unbondingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - -1, - ), - ).toThrow("Withdrawal feeRate must be bigger than 0"); - }); - - it(`${networkName} - should throw an error if the timelock script is not valid`, () => { - // mock decompile to return null - jest.spyOn(script, "decompile").mockReturnValue(null); - expect(() => - withdrawTimelockUnbondedTransaction( - { - timelockScript: Buffer.alloc(1), - slashingScript: testData.stakingScripts.slashingScript, - unbondingScript: testData.stakingScripts.unbondingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - ), - ).toThrow("Timelock script is not valid"); - }); - - it(`${networkName} - should throw an error if output index is invalid`, () => { - expect(() => - withdrawTimelockUnbondedTransaction( - { - timelockScript: testData.stakingScripts.timelockScript, - slashingScript: testData.stakingScripts.slashingScript, - unbondingScript: testData.stakingScripts.unbondingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - -1, - ), - ).toThrow("Output index must be bigger or equal to 0"); - }); - - it(`${networkName} - should throw if not enough funds to cover fees`, () => { - const { stakingTx, stakerInfo, keyPair, stakingAmountSat } = - dataGenerator.generateRandomStakingTransaction(network); - const stakingScripts = - dataGenerator.generateMockStakingScripts(keyPair); - const unitWithdrawTxFee = getWithdrawTxFee(1); - const feeRateToExceedAmount = - Math.ceil(stakingAmountSat / unitWithdrawTxFee) * 10; - - expect(() => - withdrawEarlyUnbondedTransaction( - { - unbondingTimelockScript: stakingScripts.unbondingTimelockScript, - slashingScript: stakingScripts.slashingScript, - }, - stakingTx, - stakerInfo.address, - network, - feeRateToExceedAmount, - ), - ).toThrow( - "Not enough funds to cover the fee for withdrawal transaction", - ); - }); - - it(`${networkName} - should throw if output is less than dust limit`, () => { - const params = dataGenerator.generateStakingParams( - false, - undefined, - 0, - ); - const estimatedFee = getWithdrawTxFee(DEFAULT_TEST_FEE_RATE); - const amountToNotCoverDustLimit = - BTC_DUST_SAT + estimatedFee - 1 + params.unbondingFeeSat; - - const { stakingTx, stakerInfo, stakingInstance, stakingTxFee } = - dataGenerator.generateRandomStakingTransaction( - network, - DEFAULT_TEST_FEE_RATE, - undefined, - amountToNotCoverDustLimit, - undefined, - params, - ); - const { transaction: unbondingTx } = - stakingInstance.createUnbondingTransaction(stakingTx); - expect(() => - withdrawEarlyUnbondedTransaction( - stakingInstance.buildScripts(), - unbondingTx, - stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - ), - ).toThrow("Output value is less than dust limit"); - }); - - it(`${networkName} - should return a valid psbt result for early unbonded transaction`, () => { - const psbtResult = withdrawEarlyUnbondedTransaction( - { - unbondingTimelockScript: - testData.stakingScripts.unbondingTimelockScript, - slashingScript: testData.stakingScripts.slashingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - ); - validateCommonFields(psbtResult, testData.stakerInfo.address); - }); - - it(`${networkName} - should return a valid psbt result for timelock unbonded transaction`, () => { - const psbtResult = withdrawTimelockUnbondedTransaction( - { - timelockScript: testData.stakingScripts.timelockScript, - slashingScript: testData.stakingScripts.slashingScript, - unbondingScript: testData.stakingScripts.unbondingScript, - }, - testData.stakingTx, - testData.stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - ); - validateCommonFields(psbtResult, testData.stakerInfo.address); - }); - - it(`${networkName} - should create the withdraw slashing transactions successfully`, () => { - const slashingTypes: SlashingType[] = [ - "earlyUnbonded", - "timelockExpire", - ]; - slashingTypes.forEach((type) => { - const { tx: slashingTx } = - dataGenerator.generateSlashingTransaction( - network, - testData.stakingScripts, - testData.stakingTx, - { - minSlashingTxFeeSat: - testData.params.slashing?.minSlashingTxFeeSat!!, - slashingPkScriptHex: - testData.params.slashing?.slashingPkScriptHex!!, - slashingRate: testData.params.slashing?.slashingRate!!, - }, - testData.keyPair, - type, - ); - - const outputIndex = findMatchingTxOutputIndex( - slashingTx, - deriveSlashingOutput(testData.stakingScripts, network) - .outputAddress, - network, - ); - - const psbt = withdrawSlashingTransaction( - testData.stakingScripts, - slashingTx, - testData.stakerInfo.address, - network, - DEFAULT_TEST_FEE_RATE, - outputIndex, - ); - validateCommonFields(psbt, testData.stakerInfo.address); - - // Validate the slashing output value - const remainingAmout = - slashingTx.outs[outputIndex].value - - getWithdrawTxFee(DEFAULT_TEST_FEE_RATE); - expect(psbt.psbt.txOutputs[0].value).toBe( - Math.floor(remainingAmout), - ); - }); - }); - }, - ); - }, -); - -const validateCommonFields = ( - psbtResult: PsbtResult, - withdrawalAddress: string, -) => { - expect(psbtResult).toBeDefined(); - const { psbt, fee } = psbtResult; - const inputAmount = psbt.data.inputs.reduce( - (sum, input) => sum + input.witnessUtxo!.value, - 0, - ); - const outputAmount = psbt.txOutputs.reduce( - (sum, output) => sum + output.value, - 0, - ); - expect(inputAmount).toBeGreaterThan(outputAmount); - expect(inputAmount - outputAmount).toEqual(fee); - expect( - psbt.txOutputs.find((output) => output.address === withdrawalAddress), - ).toBeDefined(); - - // validate the psbt version - expect(psbt.version).toBe(TRANSACTION_VERSION); - // validate the locktime - expect(psbt.locktime).toBe(0); -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts deleted file mode 100644 index ac06ea0127..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/staking/validation.test.ts +++ /dev/null @@ -1,367 +0,0 @@ -import * as utils from '../../src/utils/staking'; -import { testingNetworks } from '../helper'; -import { Staking } from "../../src/staking"; - -describe.each(testingNetworks)("Staking input validations", ({ - network, datagen: { stakingDatagen: dataGenerator } -}) => { - describe('validateDelegationInputs', () => { - const params = dataGenerator.generateStakingParams(true); - const feeRate = 1; - const { - stakingTx, timelock, stakerInfo, finalityProviderPksNoCoordHex, - } = dataGenerator.generateRandomStakingTransaction( - network, feeRate, undefined, undefined, undefined, params, - ); - - const stakingInstance = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - beforeEach(() => { - jest.restoreAllMocks(); - }); - - it('should throw an error if the timelock is out of range', () => { - expect(() => { - new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, params.minStakingTimeBlocks - 1, - ); - }).toThrow('Staking transaction timelock is out of range'); - - expect(() => { - new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, params.maxStakingTimeBlocks + 1, - ); - }).toThrow('Staking transaction timelock is out of range'); - }); - - it('should throw an error if the output index is out of range', () => { - jest.spyOn(utils, "findMatchingTxOutputIndex").mockImplementation(() => { - throw new Error('Staking transaction output index is out of range'); - }); - expect(() => { - stakingInstance.createWithdrawStakingExpiredPsbt( - stakingTx, feeRate, - ); - }).toThrow('Staking transaction output index is out of range'); - - expect(() => { - stakingInstance.createUnbondingTransaction( - stakingTx - ); - }).toThrow('Staking transaction output index is out of range'); - }); - }); - - describe('validateParams', () => { - const { publicKey, publicKeyNoCoord} = dataGenerator.generateRandomKeyPair(); - const { address } = dataGenerator.getAddressAndScriptPubKey( - publicKey, - ).taproot; - - const stakerInfo = { - address, - publicKeyNoCoordHex: publicKeyNoCoord, - publicKeyWithCoord: publicKey, - }; - const finalityProviderPksNoCoordHex: string[] = []; - for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { - finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); - } - const validParams = dataGenerator.generateStakingParams(); - - it('should pass with valid parameters', () => { - expect(() => new Staking( - network, - stakerInfo, - validParams, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).not.toThrow(); - }); - - it('should pass with valid parameters without slashing', () => { - const paramsWithoutSlashing = { ...validParams, slashing: undefined }; - expect(() => new Staking( - network, - stakerInfo, - paramsWithoutSlashing, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).not.toThrow(); - }); - - it('should throw an error if covenant public keys are empty', () => { - const params = { ...validParams, covenantNoCoordPks: [] }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Could not find any covenant public keys' - ); - }); - - it('should throw an error if covenant public keys are with coordinates', () => { - const params = { - ...validParams, - covenantNoCoordPks: validParams.covenantNoCoordPks.map(pk => '02' + pk ) - }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Covenant public key should contains no coordinate' - ); - }); - - it('should throw an error if covenant public keys are less than the quorum', () => { - const params = { ...validParams, covenantQuorum: validParams.covenantNoCoordPks.length + 1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Covenant public keys must be greater than or equal to the quorum' - ); - }); - - it('should throw an error if unbonding time is less than or equal to 0', () => { - let params = { ...validParams, unbondingTime: 0 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Unbonding time must be greater than 0' - ); - - params = { ...validParams, unbondingTime: -1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Unbonding time must be greater than 0' - ); - }); - - it('should throw an error if unbonding fee is less than or equal to 0', () => { - let params = { ...validParams, unbondingFeeSat: 0 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Unbonding fee must be greater than 0' - ); - - params = { ...validParams, unbondingFeeSat: -1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Unbonding fee must be greater than 0' - ); - }); - - it('should throw an error if max staking amount is less than min staking amount', () => { - const params = { ...validParams, maxStakingAmountSat: 500 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Max staking amount must be greater or equal to min staking amount' - ); - }); - - it('should throw an error if min staking amount is less than 1', () => { - const params = { ...validParams, minStakingAmountSat: -1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Min staking amount must be greater than unbonding fee plus 1000' - ); - - const params0 = { ...validParams, minStakingAmountSat: 0 }; - - expect(() => new Staking( - network, - stakerInfo, - params0, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Min staking amount must be greater than unbonding fee plus 1000' - ); - }); - - it('should throw an error if max staking time is less than min staking time', () => { - const params = { ...validParams, maxStakingTimeBlocks: validParams.minStakingTimeBlocks - 1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Max staking time must be greater or equal to min staking time' - ); - }); - - it('should throw an error if min staking time is less than 1', () => { - const params = { ...validParams, minStakingTimeBlocks: -1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Min staking time must be greater than 0' - ); - - const params0 = { ...validParams, minStakingTimeBlocks: 0 }; - - expect(() => new Staking( - network, - stakerInfo, - params0, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Min staking time must be greater than 0' - ); - }); - - it('should throw an error if covenant quorum is less than or equal to 0', () => { - let params = { ...validParams, covenantQuorum: 0 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Covenant quorum must be greater than 0' - ); - - params = { ...validParams, covenantQuorum: -1 }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Covenant quorum must be greater than 0' - ); - }); - - it('should throw an error if slashing rate is not within the range', () => { - const params0 = { ...validParams, slashing: { - ...validParams.slashing!, - slashingRate: 0, - } }; - - expect(() => new Staking( - network, - stakerInfo, - params0, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Slashing rate must be greater than 0' - ); - - const params1 = { ...validParams, slashing: { - ...validParams.slashing!, - slashingRate: 1.1, - } }; - - expect(() => new Staking( - network, - stakerInfo, - params1, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Slashing rate must be less or equal to 1' - ); - }); - - it('should throw an error if slashing public key scrit is empty', () => { - const params = { ...validParams, slashing: { - ...validParams.slashing!, - slashingPkScriptHex: "", - } }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Slashing public key script is missing' - ); - }); - - it('should throw an error if minSlashingTxFeeSat is not positive number', () => { - const params = { ...validParams, slashing: { - ...validParams.slashing!, - minSlashingTxFeeSat: 0, - } }; - - expect(() => new Staking( - network, - stakerInfo, - params, - finalityProviderPksNoCoordHex, - dataGenerator.generateRandomTimelock(validParams), - )).toThrow( - 'Minimum slashing transaction fee must be greater than 0' - ); - }); - }); -}); - diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts deleted file mode 100644 index b6555fab3b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/btc.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { payments } from "bitcoinjs-lib"; -import { - getPublicKeyNoCoord, - isNativeSegwit, - isTaproot, - isValidNoCoordPublicKey, - transactionIdToHash, -} from '../../src/utils/btc'; -import { networks } from 'bitcoinjs-lib'; -import { testingNetworks } from '../helper'; -import { deriveStakingOutputInfo } from '../../src/utils/staking'; -import { Staking } from '../../src/staking'; - -describe('address type', () => { - describe.each(testingNetworks)('should return true for a valid address type', - ({ network, datagen: { stakingDatagen: dataGenerator } }) => { - const addresses = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey - ); - it('should return true for a valid Taproot address', () => { - expect(isTaproot(addresses.taproot.address, network)).toBe(true); - }); - - it('should return true for a valid Native SegWit address', () => { - expect(isNativeSegwit(addresses.nativeSegwit.address, network)).toBe(true); - }); - - it('should return false for non-Taproot address', () => { - expect(isTaproot(addresses.nativeSegwit.address, network)).toBe(false); - - const legacyAddress = '16o1TKSUWXy51oDpL5wbPxnezSGWC9rMPv'; - expect(isTaproot(legacyAddress, network)).toBe(false); - - const nestedSegWidth = '3A2yqzgfxwwqxgse5rDTCQ2qmxZhMnfd5b'; - expect(isTaproot(nestedSegWidth, network)).toBe(false); - }); - - it('should return false for non-Native SegWit address', () => { - expect(isNativeSegwit(addresses.taproot.address, network)).toBe(false); - - const legacyAddress = '16o1TKSUWXy51oDpL5wbPxnezSGWC9rMPv'; - expect(isNativeSegwit(legacyAddress, network)).toBe(false); - - const nestedSegWidth = '3A2yqzgfxwwqxgse5rDTCQ2qmxZhMnfd5b'; - expect(isNativeSegwit(nestedSegWidth, network)).toBe(false); - }); - - }); - - const [mainnetDatagen, signetDatagen] = testingNetworks; - const envNetworks = [ - { - mainnetDatagen: mainnetDatagen.datagen.stakingDatagen, - signetDatagen: signetDatagen.datagen.stakingDatagen, - }, - ]; - - envNetworks.forEach(({ mainnetDatagen, signetDatagen }) => { - const mainnetAddresses = mainnetDatagen.getAddressAndScriptPubKey( - mainnetDatagen.generateRandomKeyPair().publicKey - ); - const signetAddresses = signetDatagen.getAddressAndScriptPubKey( - signetDatagen.generateRandomKeyPair().publicKey - ); - - it('should return false for a mis-matched address type in different networks', () => { - expect(isTaproot(signetAddresses.nativeSegwit.address, networks.testnet)).toBe(false); - expect(isNativeSegwit(mainnetAddresses.taproot.address, networks.bitcoin)).toBe(false); - - const legacyAddress = 'n2eq5iP3UsdfmGsJyEEMXyRGNx5ysUXLXb'; - expect(isTaproot(legacyAddress, networks.testnet)).toBe(false); - expect(isNativeSegwit(legacyAddress, networks.bitcoin)).toBe(false); - - const nestedSegWidth = '2NChmRbq92M6geBmwCXcFF8dCfmGr38FmX2'; - expect(isTaproot(nestedSegWidth, networks.testnet)).toBe(false); - expect(isNativeSegwit(nestedSegWidth, networks.bitcoin)).toBe(false); - }); - - it('should return false for an invalid address format', () => { - const invalidAddress = 'invalid_address'; - expect(isTaproot(invalidAddress, networks.bitcoin)).toBe(false); - expect(isNativeSegwit(invalidAddress, networks.bitcoin)).toBe(false); - }); - - it('should return false for an incorrect network', () => { - expect(isTaproot(mainnetAddresses.taproot.address, networks.testnet)).toBe(false); - expect(isTaproot(mainnetAddresses.taproot.address, networks.regtest)).toBe(false); - - expect(isTaproot(signetAddresses.taproot.address, networks.bitcoin)).toBe(false); - - expect(isNativeSegwit(mainnetAddresses.nativeSegwit.address, networks.testnet)).toBe(false); - expect(isNativeSegwit(mainnetAddresses.nativeSegwit.address, networks.regtest)).toBe(false); - - expect(isNativeSegwit(signetAddresses.nativeSegwit.address, networks.bitcoin)).toBe(false); - }); - }); -}); - -describe.each(testingNetworks)('public keys', ({ datagen: { - stakingDatagen: dataGenerator -}}) => { - const { publicKey, publicKeyNoCoord } = dataGenerator.generateRandomKeyPair() - describe('isValidNoCoordPublicKey', () => { - it('should return true for a valid public key without a coordinate', () => { - expect(isValidNoCoordPublicKey(publicKeyNoCoord)).toBe(true); - }); - - it('should return false for a public key with a coordinate', () => { - expect(isValidNoCoordPublicKey(publicKey)).toBe(false); - }); - - it('should return false for an invalid public key', () => { - const invalidPublicKey = 'invalid_public_key'; - expect(isValidNoCoordPublicKey(invalidPublicKey)).toBe(false); - }); - }); - - describe('getPublicKeyNoCoord', () => { - it('should return the public key without the coordinate', () => { - expect(getPublicKeyNoCoord(publicKey)).toBe(publicKeyNoCoord); - }); - - it('should return the same public key without the coordinate', () => { - expect(getPublicKeyNoCoord(publicKeyNoCoord)).toBe(publicKeyNoCoord); - }); - - it('should throw an error for an invalid public key', () => { - const invalidPublicKey = 'invalid_public_key'; - expect(() => getPublicKeyNoCoord(invalidPublicKey)).toThrow('Invalid public key without coordinate'); - }); - }); -}); - -describe.each(testingNetworks)('Derive staking output address', ({ - network, - datagen: { - stakingDatagen: dataGenerator - } -}) => { - const feeRate = 1; - // Random number of finality providers - const finalityProviderPksNoCoordHex: string[] = []; - for (let i = 0; i < dataGenerator.getRandomIntegerBetween(1, 10); i++) { - finalityProviderPksNoCoordHex.push(dataGenerator.generateRandomKeyPair().publicKeyNoCoord); - } - const { timelock, stakerInfo, params } = dataGenerator.generateRandomStakingTransaction( - network, feeRate - ); - - describe("should derive the staking output address from the scripts", () => { - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - const scripts = staking.buildScripts(); - const { outputAddress } = deriveStakingOutputInfo( - scripts, network - ); - expect(isTaproot(outputAddress, network)).toBe(true); - }); - - it("should throw an error if no address available from creation of pay-2-taproot output", () => { - jest.spyOn(payments, "p2tr").mockImplementation(() => { - return {}; - }); - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - const scripts = staking.buildScripts(); - expect(() => deriveStakingOutputInfo(scripts, network)) - .toThrow("Failed to build staking output"); - }); - - it("should throw an error if fail to create pay-2-taproot output", () => { - jest.spyOn(payments, "p2tr").mockImplementation(() => { - throw new Error("oops"); - }); - const staking = new Staking( - network, stakerInfo, - params, finalityProviderPksNoCoordHex, timelock, - ); - const scripts = staking.buildScripts(); - expect(() => deriveStakingOutputInfo(scripts, network)) - .toThrow("oops"); - }); -}); - -describe('transactionIdToHash', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - describe.each(testingNetworks)('should correctly convert transaction id to hash', - ({ datagen: { stakingDatagen: dataGenerator } }) => { - it('should correctly convert transaction id to hash', () => { - const utxos = dataGenerator.generateRandomUTXOs(1000, 5); // Generate multiple UTXOs to test - utxos.forEach(utxo => { - const txid = utxo.txid; - const expectedHash = Buffer.from(txid, 'hex').reverse(); - const result = transactionIdToHash(txid); - expect(Buffer.isBuffer(result)).toBe(true); - expect(result).toEqual(expectedHash); - expect(result.toString('hex')).toBe(expectedHash.toString('hex')); - }); - }); - - it('should throw an error if the transaction id is empty', () => { - const txId = ''; - expect(() => transactionIdToHash(txId)).toThrow("Transaction id cannot be empty"); - }); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts deleted file mode 100644 index f951d5e7ad..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingExpansionTxFee.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { BTC_DUST_SAT } from "../../../src/constants/dustSat"; -import { UTXO } from "../../../src/types/UTXO"; -import { TransactionOutput } from "../../../src/types/psbtOutputs"; -import { getStakingExpansionTxFundingUTXOAndFees } from "../../../src/utils/fee"; -import { buildStakingTransactionOutputs, deriveStakingOutputInfo } from "../../../src/utils/staking"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; - -describe.each(testingNetworks)("utils - fee - ", ( - { networkName, network, datagen }, -) => { - describe(`${networkName} - getStakingExpansionTxFundingUTXOAndFees`, () => { - Object.entries(datagen).forEach(([_, dataGenerator]) => { - const mockScripts = dataGenerator.generateMockStakingScripts(); - const feeRate = DEFAULT_TEST_FEE_RATE; - - it("should throw an error if there are no available UTXOs", () => { - const availableUTXOs: UTXO[] = []; - const outputs: TransactionOutput[] = []; - expect(() => - getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - feeRate, - outputs, - ), - ).toThrow("Insufficient funds"); - }); - - it("should throw if no UTXO can cover the required fees", () => { - const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( - 100, - 2, - ); - const outputs = buildStakingTransactionOutputs(mockScripts, network, 50000); - expect(() => - getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - feeRate, - outputs, - ), - ).toThrow( - "Insufficient funds: unable to find a UTXO to cover the fees for the staking expansion transaction.", - ); - }); - - it("should successfully select the smallest UTXO that can cover the fee", () => { - const availableUTXOs: UTXO[] = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 1000000, // Large UTXO - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: 1, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 50000, // Medium UTXO that should be selected - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: 2, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 100000, // Larger UTXO - }, - ]; - const outputs = buildStakingTransactionOutputs(mockScripts, network, 50000); - - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - feeRate, - outputs, - ); - - // Should select the smallest UTXO that can cover the fee - expect(result.selectedUTXO).toEqual(availableUTXOs[1]); // The 50000 satoshi UTXO - expect(result.fee).toBeGreaterThan(0); - expect(result.fee).toBeLessThanOrEqual(result.selectedUTXO.value); - }); - - it("should successfully return the accurate fee for native segwit input", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 10000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - expect(result.fee).toBe(463); - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - - it("should successfully return the accurate fee for taproot input", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey({ - isTaproot: true, - }), - value: 10000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - expect(result.fee).toBe(453); - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - - it("should successfully return the accurate fee without change", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 420, // Just enough to cover fee without change - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - expect(result.fee).toBe(420); // Without change output, hence smaller - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - - it("should successfully return the fee without change when remaining balance is below dust threshold", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 420 + BTC_DUST_SAT, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - expect(result.fee).toBe(420); // Without change output, hence smaller - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - - it("should successfully return the accurate fee with change output", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 420 + BTC_DUST_SAT + 1, // More than dust threshold - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - expect(result.fee).toBe(463); - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - - it("should filter out invalid UTXOs with non-decompilable scripts", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: "invalid_script", // Invalid script - value: 10000, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: 1, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 10000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - - // Should select the valid UTXO - expect(result.selectedUTXO).toEqual(availableUTXOs[1]); - expect(result.fee).toEqual(463); - }); - - it("should throw error when no valid UTXOs are available", () => { - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: "invalid_script_1", // Invalid script - value: 10000, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: 1, - scriptPubKey: "invalid_script_2", // Invalid script - value: 10000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, 2000); - expect(() => - getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - feeRate, - outputs, - ), - ).toThrow("Insufficient funds: no valid UTXOs available for staking"); - }); - - it("should handle multiple outputs correctly in fee calculation", () => { - const availableUTXOs: UTXO[] = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - scriptPubKey: dataGenerator.generateRandomScriptPubKey(), - value: 10000, - }, - ]; - - // Create multiple outputs to test fee calculation - const stakingOutputInfo = deriveStakingOutputInfo(mockScripts, network); - const outputs: TransactionOutput[] = [ - { - scriptPubKey: stakingOutputInfo.scriptPubKey, - value: 2000, - }, - { - scriptPubKey: Buffer.from( - dataGenerator.generateRandomScriptPubKey(), "hex" - ), - value: 1000, - }, - ]; - - const result = getStakingExpansionTxFundingUTXOAndFees( - availableUTXOs, - 1, - outputs, - ); - expect(result.fee).toBe(506); - expect(result.selectedUTXO).toEqual(availableUTXOs[0]); - }); - }); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts deleted file mode 100644 index c8fc1c1441..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/stakingtxFee.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { UTXO } from "../../../src/types/UTXO"; -import { TransactionOutput } from "../../../src/types/psbtOutputs"; -import { getStakingTxInputUTXOsAndFees } from "../../../src/utils/fee"; -import { buildStakingTransactionOutputs } from "../../../src/utils/staking"; -import { DEFAULT_TEST_FEE_RATE, testingNetworks } from "../../helper"; - -describe.each(testingNetworks)("utils - fee - ", ( - { networkName, network, datagen }, -) => { - describe(`${networkName} - getStakingTxInputUTXOsAndFees`, () => { - Object.entries(datagen).forEach(([key, dataGenerator]) => { - const mockScripts = dataGenerator.generateMockStakingScripts(); - const feeRate = DEFAULT_TEST_FEE_RATE; - const randomAmount = Math.floor(Math.random() * 100000000) + 1000; - - it("should throw an error if there are no available UTXOs", () => { - const availableUTXOs: UTXO[] = []; - const outputs: TransactionOutput[] = []; - expect(() => - getStakingTxInputUTXOsAndFees( - availableUTXOs, - randomAmount, - feeRate, - outputs, - ), - ).toThrow("Insufficient funds"); - }); - - it("should throw if total utxos value can not cover the staking value + fee", () => { - const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( - randomAmount + 1, - Math.floor(Math.random() * 10) + 1, - ); - const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); - expect(() => - getStakingTxInputUTXOsAndFees( - availableUTXOs, - randomAmount, - feeRate, - outputs, - ), - ).toThrow( - "Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees", - ); - }); - - it("should successfully select the correct UTXOs and calculate the fee", () => { - const availableUTXOs: UTXO[] = dataGenerator.generateRandomUTXOs( - randomAmount + 10000000, // give enough satoshis to cover the fee - Math.floor(Math.random() * 10) + 1, - ); - const outputs = buildStakingTransactionOutputs(mockScripts, network, randomAmount); - - const result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - randomAmount, - feeRate, - outputs, - ); - // Ensure the correct UTXOs are selected - expect(result.selectedUTXOs.length).toBeLessThanOrEqual( - availableUTXOs.length, - ); - // Ensure the highest value UTXOs are selected - const sortedUTXOs = [...availableUTXOs].sort((a, b) => b.value - a.value); - expect(result.selectedUTXOs).toEqual( - sortedUTXOs.slice(0, result.selectedUTXOs.length), - ); - expect(result.fee).toBeGreaterThan(0); - }); - - it("should successfully return the accurate fee for taproot input", () => { - const stakeAmount = 2000; - const { taproot } = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: taproot.scriptPubKey, - value: 1000, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: taproot.scriptPubKey, - value: 2000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); - // Manually setting fee rate less than 2 so that the fee calculation included ESTIMATION_ACCUARACY_BUFFER - let result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 1, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(325); // This number is calculated manually - } else { - expect(result.fee).toBe(243); // staking has less fees due to no op_return - } - expect(result.selectedUTXOs.length).toEqual(2); - - result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 2, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(534); // This number is calculated manually - } else { - expect(result.fee).toBe(456); // staking has less fees due to no op_return - } - expect(result.selectedUTXOs.length).toEqual(2); - - // Once fee rate is set to 3, the fee will be calculated with addition of TX_BUFFER_SIZE_OVERHEAD * feeRate - result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 3, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(756); // This number is calculated manually - } else { - expect(result.fee).toBe(510); - } - expect(result.selectedUTXOs.length).toEqual(2); - }); - - it("should successfully return the accurate fee for native segwit input", () => { - const stakeAmount = 2000; - const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 1000, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 2000, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); - const result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 1, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(345); // This number is calculated manually - } else { - expect(result.fee).toBe(263); // staking has less fees due to no op_return - } - expect(result.selectedUTXOs.length).toEqual(2); - }); - - it("should successfully return the accurate fee without change", () => { - const stakeAmount = 2000; - const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 1009, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 1293, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); - const result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 1, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(302); // This is the fee for 2 inputs and 2 outputs without change - } else { - expect(result.fee).toBe(220); // staking has less fees due to no op_return - } - expect(result.selectedUTXOs.length).toEqual(2); - }); - - it("should successfully return the accurate fee utilising only one of the UTXOs", () => { - const stakeAmount = 2000; - const { nativeSegwit } = dataGenerator.getAddressAndScriptPubKey( - dataGenerator.generateRandomKeyPair().publicKey, - ); - const availableUTXOs = [ - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 1000, - }, - { - txid: dataGenerator.generateRandomTxId(), - vout: Math.floor(Math.random() * 10), - scriptPubKey: nativeSegwit.scriptPubKey, - value: 2500, - }, - ]; - - const outputs = buildStakingTransactionOutputs(mockScripts, network, stakeAmount); - const result = getStakingTxInputUTXOsAndFees( - availableUTXOs, - stakeAmount, - 1, - outputs, - ); - if (key === "observableStakingDatagen") { - expect(result.fee).toBe(234); - } else { - expect(result.fee).toBe(152); // staking has less fees due to no op_return - } - expect(result.selectedUTXOs.length).toEqual(1); - }); - }); - }); -}); - diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts deleted file mode 100644 index 19bccffe75..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/utils.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { script as bitcoinScript, opcodes, payments } from "bitcoinjs-lib"; -import { - DEFAULT_INPUT_SIZE, - MAX_NON_LEGACY_OUTPUT_SIZE, - P2TR_INPUT_SIZE, - P2WPKH_INPUT_SIZE, -} from "../../../src/constants/fee"; -import { UTXO } from "../../../src/types/UTXO"; -import { - getEstimatedChangeOutputSize, - getInputSizeByScript, - inputValueSum, - isOP_RETURN, -} from "../../../src/utils/fee/utils"; -import { testingNetworks } from "../../helper"; - -describe("is OP_RETURN", () => { - it("should return true for an OP_RETURN script", () => { - const script = bitcoinScript.compile([ - opcodes.OP_RETURN, - Buffer.from("hello world"), - ]); - expect(isOP_RETURN(script)).toBe(true); - }); - - it("should return false for a non-OP_RETURN script", () => { - const script = bitcoinScript.compile([ - opcodes.OP_DUP, - opcodes.OP_HASH160, - Buffer.alloc(20), - opcodes.OP_EQUALVERIFY, - opcodes.OP_CHECKSIG, - ]); - expect(isOP_RETURN(script)).toBe(false); - }); - - it("should return false for an invalid script", () => { - const script = Buffer.from("invalidscript", "hex"); - expect(isOP_RETURN(script)).toBe(false); - }); -}); - -describe.each(testingNetworks)("scriptUtils", ({ - networkName, - datagen, -}) => { - describe.each(Object.values(datagen))(`${networkName} - getInputSizeByScript`, ( - dataGenerator - ) => { - it("should return P2WPKH_INPUT_SIZE for a valid P2WPKH script", () => { - const pk = dataGenerator.generateRandomKeyPair().publicKey; - const { output } = payments.p2wpkh({ pubkey: Buffer.from(pk, "hex") }); - if (output) { - expect(getInputSizeByScript(output)).toBe(P2WPKH_INPUT_SIZE); - } - }); - - it("should return P2TR_INPUT_SIZE for a valid P2TR script", () => { - const pk = dataGenerator.generateRandomKeyPair().publicKeyNoCoord; - const { output } = payments.p2tr({ - internalPubkey: Buffer.from(pk, "hex"), - }); - expect(getInputSizeByScript(output!)).toBe(P2TR_INPUT_SIZE); - }); - - it("should return DEFAULT_INPUT_SIZE for an invalid or unrecognized script", () => { - const script = bitcoinScript.compile([ - opcodes.OP_DUP, - opcodes.OP_HASH160, - Buffer.alloc(20), - opcodes.OP_EQUALVERIFY, - opcodes.OP_CHECKSIG, - ]); - expect(getInputSizeByScript(script)).toBe(DEFAULT_INPUT_SIZE); - }); - - it("should handle malformed scripts gracefully and return DEFAULT_INPUT_SIZE", () => { - const malformedScript = Buffer.from("00", "hex"); - expect(getInputSizeByScript(malformedScript)).toBe(DEFAULT_INPUT_SIZE); - }); - }); -}); - -describe("getEstimatedChangeOutputSize", () => { - it("should return correct value for the estimated change output size", () => { - expect(getEstimatedChangeOutputSize()).toBe(MAX_NON_LEGACY_OUTPUT_SIZE); - }); -}); - -describe("inputValueSum", () => { - it("should return the correct sum of UTXO values", () => { - const inputUTXOs: UTXO[] = [ - { txid: "txid1", vout: 0, value: 5000, scriptPubKey: "script1" }, - { txid: "txid2", vout: 1, value: 10000, scriptPubKey: "script2" }, - ]; - const expectedSum = 15000; - const actualSum = inputValueSum(inputUTXOs); - expect(actualSum).toBe(expectedSum); - }); - - it("should return zero for an empty UTXO list", () => { - const inputUTXOs: UTXO[] = []; - const expectedSum = 0; - const actualSum = inputValueSum(inputUTXOs); - expect(actualSum).toBe(expectedSum); - }); - - it("should return the correct sum for UTXOs with varying values", () => { - const inputUTXOs: UTXO[] = [ - { txid: "txid1", vout: 0, value: 2500, scriptPubKey: "script1" }, - { txid: "txid2", vout: 1, value: 7500, scriptPubKey: "script2" }, - { txid: "txid3", vout: 2, value: 10000, scriptPubKey: "script3" }, - ]; - const expectedSum = 20000; - const actualSum = inputValueSum(inputUTXOs); - expect(actualSum).toBe(expectedSum); - }); - - it("should handle large UTXO values correctly", () => { - const inputUTXOs: UTXO[] = [ - { txid: "txid1", vout: 0, value: 2 ** 53 - 1, scriptPubKey: "script1" }, - { txid: "txid2", vout: 1, value: 1, scriptPubKey: "script2" }, - ]; - const expectedSum = 2 ** 53 - 1 + 1; - const actualSum = inputValueSum(inputUTXOs); - expect(actualSum).toBe(expectedSum); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts deleted file mode 100644 index b050f26aa2..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/fee/withdrawTxFee.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - LOW_RATE_ESTIMATION_ACCURACY_BUFFER, - MAX_NON_LEGACY_OUTPUT_SIZE, - P2TR_INPUT_SIZE, - TX_BUFFER_SIZE_OVERHEAD, - WITHDRAW_TX_BUFFER_SIZE, -} from "../../../src/constants/fee"; -import { getWithdrawTxFee } from "../../../src/utils/fee"; - -describe("getWithdrawTxFee", () => { - it("should calculate the correct withdraw transaction fee for a given fee rate", () => { - const feeRate = Math.floor(Math.random() * 100); - let expectedTotalFee = - feeRate * - (P2TR_INPUT_SIZE + - MAX_NON_LEGACY_OUTPUT_SIZE + - WITHDRAW_TX_BUFFER_SIZE + - TX_BUFFER_SIZE_OVERHEAD); - if (feeRate <= 2) { - expectedTotalFee += LOW_RATE_ESTIMATION_ACCURACY_BUFFER; - } - const actualFee = getWithdrawTxFee(feeRate); - - expect(actualFee).toBe(expectedTotalFee); - }); -}); diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts deleted file mode 100644 index c6644618b0..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/pop.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { sha256 } from "bitcoinjs-lib/src/crypto"; - -import { - createStakerPopContext, - buildPopMessage, -} from "../../src/utils/pop"; -import { PopUpgradeConfig } from "../../src/types"; -import { STAKING_MODULE_ADDRESS } from "../../src/constants/staking"; -import { babylonAddress } from "../staking/manager/__mock__/fee"; -import { mockChainId } from "../staking/manager/__mock__/providers"; - -describe("POP Utility Functions", () => { - const mockBech32Address = babylonAddress; - - describe("createStakerPopContext", () => { - it("should generate correct context hash with default version", () => { - const contextHash = createStakerPopContext(mockChainId); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - - expect(contextHash).toBe(expectedHash); - }); - - it("should generate correct context hash with custom version", () => { - const contextHash = createStakerPopContext(mockChainId, 1); - - const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - - expect(contextHash).toBe(expectedHash); - }); - - it("should generate correct context hash with version 2", () => { - const contextHash = createStakerPopContext(mockChainId, 2); - - const expectedContextString = `btcstaking/2/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - - expect(contextHash).toBe(expectedHash); - }); - - it("should handle empty chain ID", () => { - const contextHash = createStakerPopContext(""); - - const expectedContextString = `btcstaking/0/staker_pop//${STAKING_MODULE_ADDRESS}`; - const expectedHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - - expect(contextHash).toBe(expectedHash); - }); - }); - - describe("buildPopMessage", () => { - const mockUpgradeConfig: PopUpgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - - describe("Legacy Format", () => { - it("should return bech32 address when no upgrade config provided", () => { - const currentHeight = 300; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - ); - - expect(result).toBe(mockBech32Address); - }); - - it("should return bech32 address when current height is below upgrade height", () => { - const currentHeight = 100; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - expect(result).toBe(mockBech32Address); - }); - - it("should return bech32 address when upgrade height is undefined", () => { - const result = buildPopMessage( - mockBech32Address, - 300, - mockChainId, - ); - - expect(result).toBe(mockBech32Address); - }); - }); - - describe("New Format", () => { - it("should return context hash + address when current height equals upgrade height", () => { - const currentHeight = 200; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should return context hash + address when current height is above upgrade height", () => { - const currentHeight = 300; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should use custom version when provided", () => { - const currentHeight = 300; - const customConfig = { - upgradeHeight: 200, - version: 1, - }; - - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - customConfig, - ); - - const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should always use new format when upgrade height is 0", () => { - const currentHeight = 100; - const customConfig: PopUpgradeConfig = { - upgradeHeight: 0, - version: 0, - }; - - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - customConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty bech32 address", () => { - const currentHeight = 300; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - - const result = buildPopMessage( - "", - 300, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + ""; - - expect(result).toBe(expectedMessage); - }); - - it("should handle very large height values", () => { - const currentHeight = Number.MAX_SAFE_INTEGER; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should handle empty chain ID", () => { - const currentHeight = 300; - const upgradeConfig = { - upgradeHeight: mockUpgradeConfig.upgradeHeight, - version: mockUpgradeConfig.version, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - "", - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop//${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - }); - }); - - describe("buildPopMessage (createPopMessageToSign replacement)", () => { - describe("Success Cases", () => { - it("should return legacy format when no upgrade config provided", () => { - const currentHeight = 300; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - ); - - expect(result).toBe(mockBech32Address); - }); - - it("should return legacy format when height is below upgrade height", () => { - const currentHeight = 100; - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - expect(result).toBe(mockBech32Address); - }); - - it("should return new format when height is above upgrade height", () => { - const currentHeight = 300; - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should use custom version when provided", () => { - const currentHeight = 300; - const upgradeConfig = { - upgradeHeight: 200, - version: 1, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/1/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - }); - - describe("Edge Cases", () => { - it("should handle empty bech32 address", () => { - const currentHeight = 100; - const upgradeConfig = { - upgradeHeight: 200, - version: 0, - }; - const result = buildPopMessage( - "", - currentHeight, - mockChainId, - upgradeConfig, - ); - - expect(result).toBe(""); - }); - - it("should handle zero height", () => { - const currentHeight = 0; - const upgradeConfig: PopUpgradeConfig = { - upgradeHeight: 0, - version: 0, - }; - - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - // Should use new format since height (0) >= upgrade height (0) - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - - it("should handle very large height values", () => { - const currentHeight = Number.MAX_SAFE_INTEGER; - const upgradeConfig = { - upgradeHeight: 1000, - version: 0, - }; - const result = buildPopMessage( - mockBech32Address, - currentHeight, - mockChainId, - upgradeConfig, - ); - - const expectedContextString = `btcstaking/0/staker_pop/${mockChainId}/${STAKING_MODULE_ADDRESS}`; - const expectedContextHash = sha256(Buffer.from(expectedContextString, "utf8")).toString("hex"); - const expectedMessage = expectedContextHash + mockBech32Address; - - expect(result).toBe(expectedMessage); - }); - }); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts deleted file mode 100644 index 794c442b4c..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/findMatchingTxOutputIndex.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Transaction, address } from "bitcoinjs-lib"; -import { findMatchingTxOutputIndex } from "../../../src/utils/staking"; -import { testingNetworks } from "../../helper"; -import { StakingError, StakingErrorCode } from "../../../src/error"; - -describe.each(testingNetworks)('findMatchingTxOutputIndex', ( - { network, networkName, datagen: { stakingDatagen: dataGenerator } } -) => { - it(`${networkName} should find the correct output index for a valid address`, () => { - // Create a transaction with multiple outputs - const tx = new Transaction(); - const keyPair1 = dataGenerator.generateRandomKeyPair(); - const keyPair2 = dataGenerator.generateRandomKeyPair(); - const outputAddress1 = dataGenerator.getAddressAndScriptPubKey(keyPair1.publicKey).nativeSegwit.address; - const outputAddress2 = dataGenerator.getAddressAndScriptPubKey(keyPair2.publicKey).nativeSegwit.address; - - // Add outputs to the transaction - tx.addOutput( - address.toOutputScript(outputAddress1, network), - 1000000 - ); - tx.addOutput( - address.toOutputScript(outputAddress2, network), - 2000000 - ); - - // Test finding the first output - const index1 = findMatchingTxOutputIndex(tx, outputAddress1, network); - expect(index1).toBe(0); - - // Test finding the second output - const index2 = findMatchingTxOutputIndex(tx, outputAddress2, network); - expect(index2).toBe(1); - }); - - it(`${networkName} should throw an error when no matching output is found`, () => { - const tx = new Transaction(); - const keyPair1 = dataGenerator.generateRandomKeyPair(); - const keyPair2 = dataGenerator.generateRandomKeyPair(); - const outputAddress1 = dataGenerator.getAddressAndScriptPubKey(keyPair1.publicKey).nativeSegwit.address; - const outputAddress2 = dataGenerator.getAddressAndScriptPubKey(keyPair2.publicKey).nativeSegwit.address; - - // Add an output with a different address - tx.addOutput( - address.toOutputScript(outputAddress1, network), - 1000000 - ); - - // Try to find an address that doesn't exist in the outputs - expect(() => { - findMatchingTxOutputIndex(tx, outputAddress2, network); - }).toThrow(new StakingError( - StakingErrorCode.INVALID_OUTPUT, - `Matching output not found for address: ${outputAddress2}` - )); - }); - - it(`${networkName} should handle empty transaction outputs`, () => { - const tx = new Transaction(); - const keyPair = dataGenerator.generateRandomKeyPair(); - const outputAddress = dataGenerator.getAddressAndScriptPubKey(keyPair.publicKey).nativeSegwit.address; - - expect(() => { - findMatchingTxOutputIndex(tx, outputAddress, network); - }).toThrow(new StakingError( - StakingErrorCode.INVALID_OUTPUT, - `Matching output not found for address: ${outputAddress}` - )); - }); - - it(`${networkName} should handle no matching address from output scripts`, () => { - const tx = new Transaction(); - const keyPair = dataGenerator.generateRandomKeyPair(); - const outputAddress = dataGenerator.getAddressAndScriptPubKey( - keyPair.publicKey - ).nativeSegwit.address; - - tx.addOutput( - Buffer.from('OP_RETURN xyz'), - 1000000 - ); - - expect(() => { - findMatchingTxOutputIndex(tx, outputAddress, network); - }).toThrow(new StakingError( - StakingErrorCode.INVALID_OUTPUT, - `Matching output not found for address: ${outputAddress}` - )); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts deleted file mode 100644 index 84ed4d1c85..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/staking/validation.test.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { validateStakingExpansionCovenantQuorum, validateStakingExpansionInputs, validateStakingTxInputData } from "../../../src/utils/staking/validation"; -import { testingNetworks } from "../../helper"; - -describe.each(testingNetworks)('validateStakingTxInputData', ( - { datagen } -) => { - describe.each(Object.values(datagen))('validateStakingTxInputData', ( - dataGenerator - ) => { - const params = dataGenerator.generateStakingParams(); - const balance = dataGenerator.getRandomIntegerBetween( - params.maxStakingAmountSat, params.maxStakingAmountSat + 100000000, - ); - const numberOfUTXOs = dataGenerator.getRandomIntegerBetween(1, 10); - const validInputUTXOs = dataGenerator.generateRandomUTXOs(balance, numberOfUTXOs); - const feeRate = 1; - - it('should pass with valid staking amount, term, UTXOs, and fee rate', () => { - expect(() => - validateStakingTxInputData( - params.minStakingAmountSat, - params.minStakingTimeBlocks, - params, - validInputUTXOs, - feeRate, - ) - ).not.toThrow(); - }); - - it('should throw an error if staking amount is less than the minimum', () => { - expect(() => - validateStakingTxInputData( - params.minStakingAmountSat -1 , - params.minStakingTimeBlocks, - params, - validInputUTXOs, - feeRate, - ) - ).toThrow('Invalid staking amount'); - }); - - it('should throw an error if staking amount is greater than the maximum', () => { - expect(() => - validateStakingTxInputData( - params.maxStakingAmountSat + 1 , - params.minStakingTimeBlocks, - params, - validInputUTXOs, - feeRate, - ) - ).toThrow('Invalid staking amount'); - }); - - it('should throw an error if time lock is less than the minimum', () => { - expect(() => - validateStakingTxInputData( - params.maxStakingAmountSat, - params.minStakingTimeBlocks -1 , - params, - validInputUTXOs, - feeRate, - ) - ).toThrow('Invalid timelock'); - }); - - it('should throw an error if time lock is greater than the maximum', () => { - expect(() => - validateStakingTxInputData( - params.maxStakingAmountSat, - params.maxStakingTimeBlocks + 1 , - params, - validInputUTXOs, - feeRate, - ) - ).toThrow('Invalid timelock'); - }); - - it('should throw an error if no input UTXOs are provided', () => { - expect(() => - validateStakingTxInputData( - params.maxStakingAmountSat, - params.maxStakingTimeBlocks, - params, - [], - feeRate, - ) - ).toThrow('No input UTXOs provided'); - }); - - it('should throw an error if fee rate is less than or equal to zero', () => { - expect(() => - validateStakingTxInputData( - params.maxStakingAmountSat, - params.maxStakingTimeBlocks, - params, - validInputUTXOs, - 0, - ) - ).toThrow('Invalid fee rate'); - }); - }); -}); - -describe("validateStakingExpansionInputs", () => { - const [mainnet] = testingNetworks; - const { datagen: { stakingDatagen }, network } = mainnet; - - const babylonAddress = "bbn1cyqgpk0nlsutlm5ymkfpya30fqntanc8slpure"; - const stakerKeyPair = stakingDatagen.generateRandomKeyPair(); - const { - stakingAmountSat, - finalityProviderPksNoCoordHex, - timelock, - utxos - } = stakingDatagen.generateRandomStakingTransaction(network, 1, stakerKeyPair); - - it("should pass with valid staking expansion inputs", () => { - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 1, - inputUTXOs: utxos, - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex: finalityProviderPksNoCoordHex.slice(0, 1), - stakingAmountSat, - stakingTimelock: timelock, - }, - babylonAddress: babylonAddress, - }) - ).not.toThrow(); - }); - - it("should throw an error if the previous staking has more finality providers than the current staking", () => { - const extraFp = stakingDatagen.generateRandomFidelityProviderPksNoCoordHex(1); - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 1, - inputUTXOs: utxos, - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex: [ - ...finalityProviderPksNoCoordHex, - ...extraFp, - ], - stakingAmountSat, - stakingTimelock: timelock, - }, - babylonAddress: babylonAddress, - }) - ).toThrow( - `Invalid staking expansion: all finality providers from the previous - staking must be included. Missing: ${extraFp.join(", ")}`, - ); - }); - - it("should throw an error if the babylon address is invalid", () => { - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 1, - inputUTXOs: utxos, - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - babylonAddress: "invalid", - }) - ).toThrow("Invalid Babylon address"); - }); - - it("should throw an error if the babylon BTC tip height is 0", () => { - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 0, - inputUTXOs: utxos, - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - }) - ).toThrow("Babylon BTC tip height cannot be 0"); - }); - - it("should throw an error if no input UTXOs are provided", () => { - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 1, - inputUTXOs: [], - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - babylonAddress: babylonAddress, - }) - ).toThrow("No input UTXOs provided"); - }); - - it("should throw an error if the staking amount is not equal to the previous staking amount", () => { - expect(() => - validateStakingExpansionInputs({ - babylonBtcTipHeight: 1, - inputUTXOs: utxos, - stakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat: stakingAmountSat - 1, - stakingTimelock: timelock, - }, - previousStakingInput: { - finalityProviderPksNoCoordHex, - stakingAmountSat, - stakingTimelock: timelock, - }, - babylonAddress: babylonAddress, - }) - ).toThrow("Staking expansion amount must equal the previous staking amount"); - }); -}); - -describe.each(testingNetworks)("validateStakingExpansionCovenantQuorum", ( - { datagen: { stakingDatagen } } -) => { - const previousParams = stakingDatagen.generateStakingParams(true, 10); - const { - covenantQuorum: requiredQuorum, - } = previousParams; - - it("should pass with valid same staking parameters", () => { - expect(() => - validateStakingExpansionCovenantQuorum(previousParams, previousParams) - ).not.toThrow(); - }); - - it("should throw an error if the previous staking has less covenant members than the required quorum", () => { - // Replace the previous covenant members with new ones. - // The replaced number of members matches the quorum. - const newParams = JSON.parse(JSON.stringify(previousParams)); - const newCovenantNoCoordPks = stakingDatagen.generateRandomCovenantCommittee(requiredQuorum).map( - (buffer) => buffer.toString("hex"), - ); - const { covenantNoCoordPks } = newParams; - - newParams.covenantNoCoordPks = covenantNoCoordPks.slice( - requiredQuorum, covenantNoCoordPks.length - ).concat(newCovenantNoCoordPks); - - expect(() => - validateStakingExpansionCovenantQuorum(previousParams, newParams) - ).toThrow( - `Staking expansion failed: insufficient covenant quorum. ` + - `Required: ${requiredQuorum}, Available: ${covenantNoCoordPks.length - requiredQuorum}. ` + - `Too many covenant members have rotated out.` - ); - }); - - it("should pass with number of rotated out covenant members less than the required quorum", () => { - // Replace the previous covenant members with new ones. - // The replaced number of members matches the quorum. - const newParams = JSON.parse(JSON.stringify(previousParams)); - const newCovenantNoCoordPks = stakingDatagen.generateRandomCovenantCommittee(requiredQuorum-1).map( - (buffer) => buffer.toString("hex"), - ); - const { covenantNoCoordPks } = newParams; - - newParams.covenantNoCoordPks = covenantNoCoordPks.slice( - 0, requiredQuorum - ).concat(newCovenantNoCoordPks); - - expect(() => - validateStakingExpansionCovenantQuorum(previousParams, newParams) - ).not.toThrow(); - }); -}); \ No newline at end of file diff --git a/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts b/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts deleted file mode 100644 index d697addc1a..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/tests/utils/utxo/getPsbtInputFields.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import * as bitcoin from "bitcoinjs-lib"; - -import { UTXO } from "../../../src/types"; -import { getPsbtInputFields } from "../../../src/utils/utxo/getPsbtInputFields"; -import * as getScriptType from "../../../src/utils/utxo/getScriptType"; -import testingNetworks from "../../helper/testingNetworks"; - -describe.each(testingNetworks)( - "Get PSBT input fields for UTXOs", - ({ network, datagen }) => { - describe.each(Object.entries(datagen))( - "using %s", - (_dataGenName, dataGenerator) => { - const dummyRawTxHex = "0200000001abcdef"; - const dummyRedeemScriptHex = "abcdef"; - const dummyValue = 10000 - - // helper for this particular tests - function makeUTXO( - scriptPubKey: string, - overrides?: Partial, - ): UTXO { - return { - txid: dataGenerator.generateRandomTxId(), - vout: 0, - value: dummyValue, - scriptPubKey, - ...overrides, - }; - } - - // this will throw from `getScriptType` - it("throws if cannot get the script type", () => { - const unknownScript = bitcoin.script.compile([ - bitcoin.opcodes.OP_RETURN, - Buffer.from("UNKNOWN"), - ]); - - const utxo = makeUTXO(unknownScript.toString("hex")); - expect(() => getPsbtInputFields(utxo)).toThrow("Unknown script type"); - }); - - // this will throw from `getPsbtInputField` - it("throws if the script type is not supported", () => { - const unknownScript = "NOT_SUPPORTED_SCRIPT"; - - // Save the spy so we can restore it - const spy = jest - .spyOn(getScriptType, "getScriptType") - .mockImplementation(() => { - return unknownScript as any; - }); - - try { - const notSupportedScript = bitcoin.script.compile([ - bitcoin.opcodes.OP_RETURN, - Buffer.from("UNKNOWN"), - ]); - const utxo = makeUTXO(notSupportedScript.toString("hex")); - expect(() => getPsbtInputFields(utxo)).toThrow( - `Unsupported script type: ${unknownScript}`, - ); - } finally { - // Restore the original implementation, otherwise it will affect other tests - spy.mockRestore(); - } - }); - - describe("P2PKH", () => { - it("returns nonWitnessUtxo when rawTxHex is provided", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const p2pkh = bitcoin.payments.p2pkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - // Must include rawTxHex for legacy inputs - const utxo = makeUTXO(p2pkh.output!.toString("hex"), { - rawTxHex: dummyRawTxHex, - }); - - const fields = getPsbtInputFields(utxo); - expect(fields.nonWitnessUtxo).toEqual( - Buffer.from(dummyRawTxHex, "hex"), - ); - // Should not have witnessUtxo, redeemScript, or witnessScript - expect(fields.witnessUtxo).toBeUndefined(); - expect(fields.redeemScript).toBeUndefined(); - expect(fields.witnessScript).toBeUndefined(); - }); - - it("throws an error if rawTxHex is missing for P2PKH", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const p2pkh = bitcoin.payments.p2pkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - const utxo = makeUTXO(p2pkh.output!.toString("hex")); - expect(() => getPsbtInputFields(utxo)).toThrow( - "Missing rawTxHex for legacy P2PKH input", - ); - }); - }); - - describe("P2SH", () => { - it("returns nonWitnessUtxo and redeemScript for valid P2SH", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const nested = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); - - const utxo = makeUTXO(p2sh.output!.toString("hex"), { - rawTxHex: dummyRawTxHex, - redeemScript: dummyRedeemScriptHex, - }); - const fields = getPsbtInputFields(utxo); - expect(fields.nonWitnessUtxo).toEqual( - Buffer.from(dummyRawTxHex, "hex"), - ); - expect(fields.redeemScript).toEqual( - Buffer.from(dummyRedeemScriptHex, "hex"), - ); - expect(fields.witnessUtxo).toBeUndefined(); - }); - - it("throws if rawTxHex is missing for P2SH", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const nested = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); - - const utxo = makeUTXO(p2sh.output!.toString("hex"), { - // No rawTxHex - redeemScript: dummyRedeemScriptHex, - }); - expect(() => getPsbtInputFields(utxo)).toThrow( - "Missing rawTxHex for P2SH input", - ); - }); - - it("throws if redeemScript is missing for P2SH", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const nested = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - const p2sh = bitcoin.payments.p2sh({ redeem: nested, network }); - - const utxo = makeUTXO(p2sh.output!.toString("hex"), { - rawTxHex: dummyRawTxHex, - // No redeemScript - }); - expect(() => getPsbtInputFields(utxo)).toThrow( - "Missing redeemScript for P2SH input", - ); - }); - }); - - describe("P2WPKH", () => { - it("returns witnessUtxo only for valid P2WPKH", () => { - const { publicKey } = dataGenerator.generateRandomKeyPair(); - const p2wpkh = bitcoin.payments.p2wpkh({ - pubkey: Buffer.from(publicKey, "hex"), - network, - }); - const utxo = makeUTXO(p2wpkh.output!.toString("hex")); - - const fields = getPsbtInputFields(utxo); - expect(fields.witnessUtxo).toBeDefined(); - expect(fields.witnessUtxo?.script).toEqual( - Buffer.from(p2wpkh.output!.toString("hex"), "hex"), - ); - expect(fields.witnessUtxo?.value).toBe(dummyValue); - expect(fields.nonWitnessUtxo).toBeUndefined(); - expect(fields.redeemScript).toBeUndefined(); - expect(fields.witnessScript).toBeUndefined(); - }); - }); - - describe("P2WSH", () => { - it("returns witnessUtxo and witnessScript for valid P2WSH with custom script", () => { - const customScript = bitcoin.script.compile([ - bitcoin.opcodes.OP_RETURN, - Buffer.from("hello"), - ]); - const p2wsh = bitcoin.payments.p2wsh({ - redeem: { output: customScript }, - network, - }); - - const utxo = makeUTXO(p2wsh.output!.toString("hex"), { - witnessScript: customScript.toString("hex"), - }); - - const fields = getPsbtInputFields(utxo); - expect(fields.witnessUtxo).toEqual({ - script: Buffer.from(p2wsh.output!.toString("hex"), "hex"), - value: dummyValue, - }); - expect(fields.witnessScript).toEqual( - Buffer.from(customScript.toString("hex"), "hex"), - ); - expect(fields.nonWitnessUtxo).toBeUndefined(); - expect(fields.redeemScript).toBeUndefined(); - }); - - it("throws if witnessScript is missing for P2WSH", () => { - const customScript = bitcoin.script.compile([ - bitcoin.opcodes.OP_RETURN, - Buffer.from("hello"), - ]); - const p2wsh = bitcoin.payments.p2wsh({ - redeem: { output: customScript }, - network, - }); - const utxo = makeUTXO(p2wsh.output!.toString("hex")); // no witnessScript - - expect(() => getPsbtInputFields(utxo)).toThrow( - "Missing witnessScript for P2WSH input", - ); - }); - }); - - describe("P2TR (Taproot)", () => { - it("returns witnessUtxo only if no publicKeyNoCoord is passed", () => { - const kp = dataGenerator.generateRandomKeyPair(); - const noCoord = Buffer.from(kp.publicKeyNoCoord, "hex"); - const p2tr = bitcoin.payments.p2tr({ - internalPubkey: noCoord, - network, - }); - const utxo = makeUTXO(p2tr.output!.toString("hex")); - - const fields = getPsbtInputFields(utxo); - expect(fields.witnessUtxo).toBeDefined(); - expect(fields.tapInternalKey).toBeUndefined(); - }); - - it("returns tapInternalKey if publicKeyNoCoord is passed", () => { - const kp = dataGenerator.generateRandomKeyPair(); - const noCoord = Buffer.from(kp.publicKeyNoCoord, "hex"); - const p2tr = bitcoin.payments.p2tr({ - internalPubkey: noCoord, - network, - }); - const utxo = makeUTXO(p2tr.output!.toString("hex")); - - const fields = getPsbtInputFields(utxo, noCoord); - expect(fields.witnessUtxo).toBeDefined(); - expect(fields.tapInternalKey).toEqual(noCoord); - }); - }); - }, - ); - }, -); From c91ca2fb14423fda9263b3f538bccf5991238975 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 4 Jul 2025 13:28:32 +0200 Subject: [PATCH 03/10] feat(babylonlabs-io-btc-staking-ts): change private fields to protected Change class fields from private to protected to enable proper inheritance in BabylonBtcStakingManager. Issue: BTC-2143 Co-authored-by: llm-git --- .../src/staking/manager.ts | 689 +++++++----------- 1 file changed, 255 insertions(+), 434 deletions(-) diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts index 099d6f2921..f226660953 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts @@ -1,65 +1,52 @@ -import { - btccheckpoint, - btcstaking, - btcstakingtx, -} from "@babylonlabs-io/babylon-proto-ts"; +import { btccheckpoint, btcstaking, btcstakingtx } from '@babylonlabs-io/babylon-proto-ts'; import { BIP322Sig, BTCSigType, ProofOfPossessionBTC, -} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; -import type { Emitter } from "nanoevents"; - -import { StakerInfo, Staking } from "."; -import { BABYLON_REGISTRY_TYPE_URLS } from "../constants/registry"; -import { StakingError, StakingErrorCode } from "../error"; -import { TransactionResult, UTXO } from "../types"; -import { ActionName } from "../types/action"; -import { Contract, ContractId } from "../types/contract"; -import { ManagerEvents } from "../types/events"; -import { - BabylonProvider, - BtcProvider, - InclusionProof, - StakingInputs, - UpgradeConfig, -} from "../types/manager"; -import { StakingParams, VersionedStakingParams } from "../types/params"; -import { reverseBuffer } from "../utils"; -import { isValidBabylonAddress } from "../utils/babylon"; -import { isNativeSegwit, isTaproot } from "../utils/btc"; -import { buildPopMessage } from "../utils/pop"; +} from '@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop'; +import { Psbt, Transaction, networks } from 'bitcoinjs-lib'; +import type { Emitter } from 'nanoevents'; + +import { StakerInfo, Staking } from '.'; +import { BABYLON_REGISTRY_TYPE_URLS } from '../constants/registry'; +import { StakingError, StakingErrorCode } from '../error'; +import { TransactionResult, UTXO } from '../types'; +import { ActionName } from '../types/action'; +import { Contract, ContractId } from '../types/contract'; +import { ManagerEvents } from '../types/events'; +import { BabylonProvider, BtcProvider, InclusionProof, StakingInputs, UpgradeConfig } from '../types/manager'; +import { StakingParams, VersionedStakingParams } from '../types/params'; +import { reverseBuffer } from '../utils'; +import { isValidBabylonAddress } from '../utils/babylon'; +import { isNativeSegwit, isTaproot } from '../utils/btc'; +import { buildPopMessage } from '../utils/pop'; import { clearTxSignatures, deriveMerkleProof, deriveStakingOutputInfo, extractFirstSchnorrSignatureFromTransaction, findMatchingTxOutputIndex, -} from "../utils/staking"; -import { - getBabylonParamByBtcHeight, - getBabylonParamByVersion, -} from "../utils/staking/param"; +} from '../utils/staking'; +import { getBabylonParamByBtcHeight, getBabylonParamByVersion } from '../utils/staking/param'; -import { createCovenantWitness } from "./transactions"; -import { validateStakingExpansionInputs } from "../utils/staking/validation"; +import { createCovenantWitness } from './transactions'; +import { validateStakingExpansionInputs } from '../utils/staking/validation'; export class BabylonBtcStakingManager { private upgradeConfig?: UpgradeConfig; constructor( - private network: networks.Network, - private stakingParams: VersionedStakingParams[], - private btcProvider: BtcProvider, - private babylonProvider: BabylonProvider, - private ee?: Emitter, - upgradeConfig?: UpgradeConfig, + protected network: networks.Network, + protected stakingParams: VersionedStakingParams[], + protected btcProvider: BtcProvider, + protected babylonProvider: BabylonProvider, + protected ee?: Emitter, + upgradeConfig?: UpgradeConfig ) { this.network = network; if (stakingParams.length === 0) { - throw new Error("No staking parameters provided"); + throw new Error('No staking parameters provided'); } this.stakingParams = stakingParams; @@ -88,55 +75,48 @@ export class BabylonBtcStakingManager { babylonBtcTipHeight: number, inputUTXOs: UTXO[], feeRate: number, - babylonAddress: string, + babylonAddress: string ): Promise<{ signedBabylonTx: Uint8Array; stakingTx: Transaction; }> { if (babylonBtcTipHeight === 0) { - throw new Error("Babylon BTC tip height cannot be 0"); + throw new Error('Babylon BTC tip height cannot be 0'); } if (inputUTXOs.length === 0) { - throw new Error("No input UTXOs provided"); + throw new Error('No input UTXOs provided'); } if (!isValidBabylonAddress(babylonAddress)) { - throw new Error("Invalid Babylon address"); + throw new Error('Invalid Babylon address'); } // Get the Babylon params based on the BTC tip height from Babylon chain - const params = getBabylonParamByBtcHeight( - babylonBtcTipHeight, - this.stakingParams, - ); + const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); // Create unsigned staking transaction - const { transaction } = staking.createStakingTransaction( - stakingInput.stakingAmountSat, - inputUTXOs, - feeRate, - ); + const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); // Create delegation message without including inclusion proof const msg = await this.createBtcDelegationMsg( - "delegation:create", + 'delegation:create', staking, stakingInput, transaction, babylonAddress, stakerBtcInfo, - params, + params ); - this.ee?.emit("delegation:create", { - type: "create-btc-delegation-msg", + this.ee?.emit('delegation:create', { + type: 'create-btc-delegation-msg', }); return { @@ -158,52 +138,41 @@ export class BabylonBtcStakingManager { babylonAddress: string, // Previous staking transaction info previousStakingTxInfo: { - stakingTx: Transaction, - paramVersion: number, - stakingInput: StakingInputs, - }, + stakingTx: Transaction; + paramVersion: number; + stakingInput: StakingInputs; + } ): Promise<{ signedBabylonTx: Uint8Array; stakingTx: Transaction; }> { // Perform validation for the staking expansion inputs - validateStakingExpansionInputs( - { - babylonBtcTipHeight, - inputUTXOs, - stakingInput, - previousStakingInput: previousStakingTxInfo.stakingInput, - babylonAddress, - }, - ); - // Param for the expandsion staking transaction - const params = getBabylonParamByBtcHeight( + validateStakingExpansionInputs({ babylonBtcTipHeight, - this.stakingParams, - ); + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + babylonAddress, + }); + // Param for the expandsion staking transaction + const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams); - const paramsForPreviousStakingTx = getBabylonParamByVersion( - previousStakingTxInfo.paramVersion, - this.stakingParams, - ); + const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams); const stakingInstance = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { - transaction: stakingExpansionTx, - fundingUTXO, - } = stakingInstance.createStakingExpansionTransaction( + const { transaction: stakingExpansionTx, fundingUTXO } = stakingInstance.createStakingExpansionTransaction( stakingInput.stakingAmountSat, inputUTXOs, feeRate, paramsForPreviousStakingTx, - previousStakingTxInfo, + previousStakingTxInfo ); let fundingTx; try { @@ -212,13 +181,13 @@ export class BabylonBtcStakingManager { throw StakingError.fromUnknown( error, StakingErrorCode.INVALID_INPUT, - "Failed to retrieve funding transaction hex", + 'Failed to retrieve funding transaction hex' ); } // Create delegation message without including inclusion proof const msg = await this.createBtcDelegationMsg( - "delegation:expand", + 'delegation:expand', stakingInstance, stakingInput, stakingExpansionTx, @@ -230,11 +199,11 @@ export class BabylonBtcStakingManager { previousStakingTx: previousStakingTxInfo.stakingTx, fundingTx: Transaction.fromHex(fundingTx), }, - }, + } ); - this.ee?.emit("delegation:expand", { - type: "create-btc-delegation-msg", + this.ee?.emit('delegation:expand', { + type: 'create-btc-delegation-msg', }); return { @@ -245,7 +214,7 @@ export class BabylonBtcStakingManager { /** * Estimates the transaction fee for a BTC staking expansion transaction. - * + * * @param {StakerInfo} stakerBtcInfo - The staker's Bitcoin information * including address and public key * @param {number} babylonBtcTipHeight - The current Babylon BTC tip height @@ -268,36 +237,28 @@ export class BabylonBtcStakingManager { inputUTXOs: UTXO[], feeRate: number, previousStakingTxInfo: { - stakingTx: Transaction, - paramVersion: number, - stakingInput: StakingInputs, - }, + stakingTx: Transaction; + paramVersion: number; + stakingInput: StakingInputs; + } ): number { // Validate all input parameters before fee calculation - validateStakingExpansionInputs( - { - babylonBtcTipHeight, - inputUTXOs, - stakingInput, - previousStakingInput: previousStakingTxInfo.stakingInput, - }, - ); + validateStakingExpansionInputs({ + babylonBtcTipHeight, + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + }); // Get the appropriate staking parameters based on the current Babylon BTC // tip height. This ensures we use the correct parameters for the current // network state - const params = getBabylonParamByBtcHeight( - babylonBtcTipHeight, - this.stakingParams, - ); - + const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams); + // Get the staking parameters that were used in the previous staking // transaction. This is needed to properly reconstruct the previous staking // scripts - const paramsForPreviousStakingTx = getBabylonParamByVersion( - previousStakingTxInfo.paramVersion, - this.stakingParams, - ); + const paramsForPreviousStakingTx = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams); // Create a Staking instance for the new expansion with current parameters // This will be used to build the new staking scripts and calculate the @@ -307,16 +268,14 @@ export class BabylonBtcStakingManager { stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { - fee, - } = stakingInstance.createStakingExpansionTransaction( + const { fee } = stakingInstance.createStakingExpansionTransaction( stakingInput.stakingAmountSat, inputUTXOs, feeRate, paramsForPreviousStakingTx, - previousStakingTxInfo, + previousStakingTxInfo ); return fee; @@ -344,18 +303,15 @@ export class BabylonBtcStakingManager { stakingTxHeight: number, stakingInput: StakingInputs, inclusionProof: InclusionProof, - babylonAddress: string, + babylonAddress: string ): Promise<{ signedBabylonTx: Uint8Array; }> { // Get the Babylon params at the time of the staking transaction - const params = getBabylonParamByBtcHeight( - stakingTxHeight, - this.stakingParams, - ); + const params = getBabylonParamByBtcHeight(stakingTxHeight, this.stakingParams); if (!isValidBabylonAddress(babylonAddress)) { - throw new Error("Invalid Babylon address"); + throw new Error('Invalid Babylon address'); } const stakingInstance = new Staking( @@ -363,7 +319,7 @@ export class BabylonBtcStakingManager { stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); // Validate if the stakingTx is valid based on the retrieved Babylon param @@ -371,15 +327,11 @@ export class BabylonBtcStakingManager { const stakingOutputInfo = deriveStakingOutputInfo(scripts, this.network); // Error will be thrown if the expected staking output address is not found // in the stakingTx - findMatchingTxOutputIndex( - stakingTx, - stakingOutputInfo.outputAddress, - this.network, - ); + findMatchingTxOutputIndex(stakingTx, stakingOutputInfo.outputAddress, this.network); // Create delegation message const delegationMsg = await this.createBtcDelegationMsg( - "delegation:register", + 'delegation:register', stakingInstance, stakingInput, stakingTx, @@ -388,16 +340,15 @@ export class BabylonBtcStakingManager { params, { inclusionProof: this.getInclusionProof(inclusionProof), - }, + } ); - this.ee?.emit("delegation:register", { - type: "create-btc-delegation-msg", + this.ee?.emit('delegation:register', { + type: 'create-btc-delegation-msg', }); return { - signedBabylonTx: - await this.babylonProvider.signTransaction(delegationMsg), + signedBabylonTx: await this.babylonProvider.signTransaction(delegationMsg), }; } @@ -420,30 +371,23 @@ export class BabylonBtcStakingManager { babylonBtcTipHeight: number, stakingInput: StakingInputs, inputUTXOs: UTXO[], - feeRate: number, + feeRate: number ): number { if (babylonBtcTipHeight === 0) { - throw new Error("Babylon BTC tip height cannot be 0"); + throw new Error('Babylon BTC tip height cannot be 0'); } // Get the param based on the tip height - const params = getBabylonParamByBtcHeight( - babylonBtcTipHeight, - this.stakingParams, - ); + const params = getBabylonParamByBtcHeight(babylonBtcTipHeight, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { fee: stakingFee } = staking.createStakingTransaction( - stakingInput.stakingAmountSat, - inputUTXOs, - feeRate, - ); + const { fee: stakingFee } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); return stakingFee; } @@ -466,15 +410,12 @@ export class BabylonBtcStakingManager { stakingInput: StakingInputs, unsignedStakingTx: Transaction, inputUTXOs: UTXO[], - stakingParamsVersion: number, + stakingParamsVersion: number ): Promise { - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); if (inputUTXOs.length === 0) { - throw new Error("No input UTXOs provided"); + throw new Error('No input UTXOs provided'); } const staking = new Staking( @@ -482,7 +423,7 @@ export class BabylonBtcStakingManager { stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); const stakingPsbt = staking.toStakingPsbt(unsignedStakingTx, inputUTXOs); @@ -501,25 +442,22 @@ export class BabylonBtcStakingManager { }, ]; - this.ee?.emit("delegation:stake", { + this.ee?.emit('delegation:stake', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, finalityProviders: stakingInput.finalityProviderPksNoCoordHex, covenantPks: params.covenantNoCoordPks, covenantThreshold: params.covenantQuorum, unbondingTimeBlocks: params.unbondingTime, stakingDuration: stakingInput.stakingTimelock, - type: "staking", + type: 'staking', }); - const signedStakingPsbtHex = await this.btcProvider.signPsbt( - stakingPsbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_STAKING_TRANSACTION, - }, + const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingPsbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_STAKING_TRANSACTION, }, - ); + }); return Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); } @@ -527,7 +465,7 @@ export class BabylonBtcStakingManager { /** * Creates a signed staking expansion transaction that is ready to be sent to * the BTC network. - * + * * @param {StakerInfo} stakerBtcInfo - The staker's BTC information including * address and public key * @param {StakingInputs} stakingInput - The staking inputs for the expansion @@ -552,33 +490,28 @@ export class BabylonBtcStakingManager { inputUTXOs: UTXO[], stakingParamsVersion: number, previousStakingTxInfo: { - stakingTx: Transaction, - paramVersion: number, - stakingInput: StakingInputs, + stakingTx: Transaction; + paramVersion: number; + stakingInput: StakingInputs; }, covenantStakingExpansionSignatures: { btcPkHex: string; sigHex: string; - }[], + }[] ): Promise { - validateStakingExpansionInputs( - { - inputUTXOs, - stakingInput, - previousStakingInput: previousStakingTxInfo.stakingInput, - }, - ); + validateStakingExpansionInputs({ + inputUTXOs, + stakingInput, + previousStakingInput: previousStakingTxInfo.stakingInput, + }); // Get the staking parameters for the current version // These parameters define the covenant committee and other staking rules - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); // Validate that input UTXOs are provided for the funding input if (inputUTXOs.length === 0) { - throw new Error("No input UTXOs provided"); + throw new Error('No input UTXOs provided'); } // Create a new staking instance with the current parameters @@ -588,13 +521,10 @@ export class BabylonBtcStakingManager { stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const previousParams = getBabylonParamByVersion( - previousStakingTxInfo.paramVersion, - this.stakingParams, - ); + const previousParams = getBabylonParamByVersion(previousStakingTxInfo.paramVersion, this.stakingParams); // Create the PSBT for the staking expansion transaction // This PSBT will have two inputs: the previous staking output and a @@ -603,7 +533,7 @@ export class BabylonBtcStakingManager { unsignedStakingExpansionTx, inputUTXOs, previousParams, - previousStakingTxInfo, + previousStakingTxInfo ); // Define the contract information for the PSBT signing @@ -623,54 +553,43 @@ export class BabylonBtcStakingManager { // Emit an event to notify listeners about the staking expansion // This can be used for logging, monitoring, or UI updates - this.ee?.emit("delegation:stake", { + this.ee?.emit('delegation:stake', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, finalityProviders: stakingInput.finalityProviderPksNoCoordHex, covenantPks: params.covenantNoCoordPks, covenantThreshold: params.covenantQuorum, unbondingTimeBlocks: params.unbondingTime, stakingDuration: stakingInput.stakingTimelock, - type: "staking", + type: 'staking', }); // Sign the PSBT using the BTC provider (wallet) // The wallet will sign the transaction based on the contract information // provided - const signedStakingPsbtHex = await this.btcProvider.signPsbt( - stakingExpansionPsbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_STAKING_TRANSACTION, - }, + const signedStakingPsbtHex = await this.btcProvider.signPsbt(stakingExpansionPsbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_STAKING_TRANSACTION, }, - ); + }); // Extract the signed transaction from the PSBT - const signedStakingExpansionTx = Psbt.fromHex( - signedStakingPsbtHex, - ).extractTransaction(); - + const signedStakingExpansionTx = Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); + // Validate that the signed transaction hash matches the unsigned // transaction hash // This ensures that the signing process didn't change the transaction // structure - if ( - signedStakingExpansionTx.getId() !== unsignedStakingExpansionTx.getId() - ) { - throw new Error( - "Staking expansion transaction hash does not match the computed hash", - ); + if (signedStakingExpansionTx.getId() !== unsignedStakingExpansionTx.getId()) { + throw new Error('Staking expansion transaction hash does not match the computed hash'); } // Add covenant committee signatures to the transaction // Convert covenant public keys from hex strings to buffers // The covenants committee is based on the params at the time of the previous // staking transaction. Hence using the previous params here. - const covenantBuffers = previousParams.covenantNoCoordPks.map((covenant) => - Buffer.from(covenant, "hex"), - ); - + const covenantBuffers = previousParams.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, 'hex')); + // Create the witness that includes both the staker's signature and covenant // signatures // The witness is the data that proves the transaction is authorized @@ -681,9 +600,9 @@ export class BabylonBtcStakingManager { signedStakingExpansionTx.ins[0].witness, covenantBuffers, covenantStakingExpansionSignatures, - previousParams.covenantQuorum, + previousParams.covenantQuorum ); - + // Overwrite the witness to include the covenant staking expansion signatures // This makes the transaction valid for submission to the Bitcoin network signedStakingExpansionTx.ins[0].witness = witness; @@ -709,24 +628,20 @@ export class BabylonBtcStakingManager { stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, - stakingTx: Transaction, + stakingTx: Transaction ): Promise { // Get the staking params at the time of the staking transaction - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { transaction: unbondingTx, fee } = - staking.createUnbondingTransaction(stakingTx); + const { transaction: unbondingTx, fee } = staking.createUnbondingTransaction(stakingTx); const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx); @@ -755,7 +670,7 @@ export class BabylonBtcStakingManager { }, ]; - this.ee?.emit("delegation:unbond", { + this.ee?.emit('delegation:unbond', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, finalityProviders: stakingInput.finalityProviderPksNoCoordHex, covenantPks: params.covenantNoCoordPks, @@ -763,22 +678,17 @@ export class BabylonBtcStakingManager { stakingDuration: stakingInput.stakingTimelock, unbondingTimeBlocks: params.unbondingTime, unbondingFeeSat: params.unbondingFeeSat, - type: "unbonding", + type: 'unbonding', }); - const signedUnbondingPsbtHex = await this.btcProvider.signPsbt( - psbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_UNBONDING_TRANSACTION, - }, + const signedUnbondingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_UNBONDING_TRANSACTION, }, - ); + }); - const signedUnbondingTx = Psbt.fromHex( - signedUnbondingPsbtHex, - ).extractTransaction(); + const signedUnbondingTx = Psbt.fromHex(signedUnbondingPsbtHex).extractTransaction(); return { transaction: signedUnbondingTx, @@ -809,35 +719,27 @@ export class BabylonBtcStakingManager { covenantUnbondingSignatures: { btcPkHex: string; sigHex: string; - }[], + }[] ): Promise { // Get the staking params at the time of the staking transaction - const params = getBabylonParamByVersion( + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); + + const { transaction: signedUnbondingTx, fee } = await this.createPartialSignedBtcUnbondingTransaction( + stakerBtcInfo, + stakingInput, stakingParamsVersion, - this.stakingParams, + stakingTx ); - const { transaction: signedUnbondingTx, fee } = - await this.createPartialSignedBtcUnbondingTransaction( - stakerBtcInfo, - stakingInput, - stakingParamsVersion, - stakingTx, - ); - // Check the computed txid of the signed unbonding transaction is the same as // the txid of the unsigned unbonding transaction if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) { - throw new Error( - "Unbonding transaction hash does not match the computed hash", - ); + throw new Error('Unbonding transaction hash does not match the computed hash'); } // Add covenant unbonding signatures // Convert the params of covenants to buffer - const covenantBuffers = params.covenantNoCoordPks.map((covenant) => - Buffer.from(covenant, "hex"), - ); + const covenantBuffers = params.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, 'hex')); const witness = createCovenantWitness( // Since unbonding transactions always have a single input and output, // we expect exactly one signature in TaprootScriptSpendSig when the @@ -845,7 +747,7 @@ export class BabylonBtcStakingManager { signedUnbondingTx.ins[0].witness, covenantBuffers, covenantUnbondingSignatures, - params.covenantQuorum, + params.covenantQuorum ); // Overwrite the witness to include the covenant unbonding signatures signedUnbondingTx.ins[0].witness = witness; @@ -873,23 +775,19 @@ export class BabylonBtcStakingManager { stakingInput: StakingInputs, stakingParamsVersion: number, earlyUnbondingTx: Transaction, - feeRate: number, + feeRate: number ): Promise { - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { psbt: unbondingPsbt, fee } = - staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate); + const { psbt: unbondingPsbt, fee } = staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate); const contracts: Contract[] = [ { @@ -901,21 +799,18 @@ export class BabylonBtcStakingManager { }, ]; - this.ee?.emit("delegation:withdraw", { + this.ee?.emit('delegation:withdraw', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, timelockBlocks: params.unbondingTime, - type: "early-unbonded", + type: 'early-unbonded', }); - const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt( - unbondingPsbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, + const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(unbondingPsbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, }, - ); + }); return { transaction: Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), @@ -942,25 +837,19 @@ export class BabylonBtcStakingManager { stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction, - feeRate: number, + feeRate: number ): Promise { - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt( - stakingTx, - feeRate, - ); + const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(stakingTx, feeRate); const contracts: Contract[] = [ { @@ -972,21 +861,18 @@ export class BabylonBtcStakingManager { }, ]; - this.ee?.emit("delegation:withdraw", { + this.ee?.emit('delegation:withdraw', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, timelockBlocks: stakingInput.stakingTimelock, - type: "staking-expired", + type: 'staking-expired', }); - const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt( - psbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, + const signedWithdrawalPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, }, - ); + }); return { transaction: Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), @@ -1013,25 +899,19 @@ export class BabylonBtcStakingManager { stakingInput: StakingInputs, stakingParamsVersion: number, slashingTx: Transaction, - feeRate: number, + feeRate: number ): Promise { - const params = getBabylonParamByVersion( - stakingParamsVersion, - this.stakingParams, - ); + const params = getBabylonParamByVersion(stakingParamsVersion, this.stakingParams); const staking = new Staking( this.network, stakerBtcInfo, params, stakingInput.finalityProviderPksNoCoordHex, - stakingInput.stakingTimelock, + stakingInput.stakingTimelock ); - const { psbt, fee } = staking.createWithdrawSlashingPsbt( - slashingTx, - feeRate, - ); + const { psbt, fee } = staking.createWithdrawSlashingPsbt(slashingTx, feeRate); const contracts: Contract[] = [ { @@ -1043,26 +923,21 @@ export class BabylonBtcStakingManager { }, ]; - this.ee?.emit("delegation:withdraw", { + this.ee?.emit('delegation:withdraw', { stakerPk: stakerBtcInfo.publicKeyNoCoordHex, timelockBlocks: params.unbondingTime, - type: "slashing", + type: 'slashing', }); - const signedWithrawSlashingPsbtHex = await this.btcProvider.signPsbt( - psbt.toHex(), - { - contracts, - action: { - name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, - }, + const signedWithrawSlashingPsbtHex = await this.btcProvider.signPsbt(psbt.toHex(), { + contracts, + action: { + name: ActionName.SIGN_BTC_WITHDRAW_TRANSACTION, }, - ); + }); return { - transaction: Psbt.fromHex( - signedWithrawSlashingPsbtHex, - ).extractTransaction(), + transaction: Psbt.fromHex(signedWithrawSlashingPsbtHex).extractTransaction(), fee, }; } @@ -1073,19 +948,16 @@ export class BabylonBtcStakingManager { * @returns The proof of possession. */ async createProofOfPossession( - channel: "delegation:create" | "delegation:register" | "delegation:expand", + channel: 'delegation:create' | 'delegation:register' | 'delegation:expand', bech32Address: string, - stakerBtcAddress: string, + stakerBtcAddress: string ): Promise { let sigType: BTCSigType = BTCSigType.ECDSA; // For Taproot or Native SegWit addresses, use the BIP322 signature scheme // in the proof of possession as it uses the same signature type as the regular // input UTXO spend. For legacy addresses, use the ECDSA signature scheme. - if ( - isTaproot(stakerBtcAddress, this.network) || - isNativeSegwit(stakerBtcAddress, this.network) - ) { + if (isTaproot(stakerBtcAddress, this.network) || isNativeSegwit(stakerBtcAddress, this.network)) { sigType = BTCSigType.BIP322; } @@ -1104,30 +976,30 @@ export class BabylonBtcStakingManager { upgradeConfig && { upgradeHeight: upgradeConfig.upgradeHeight, version: upgradeConfig.version, - }, + } ); this.ee?.emit(channel, { messageToSign, - type: "proof-of-possession", + type: 'proof-of-possession', }); const signedBabylonAddress = await this.btcProvider.signMessage( messageToSign, - sigType === BTCSigType.BIP322 ? "bip322-simple" : "ecdsa", + sigType === BTCSigType.BIP322 ? 'bip322-simple' : 'ecdsa' ); let btcSig: Uint8Array; if (sigType === BTCSigType.BIP322) { const bip322Sig = BIP322Sig.fromPartial({ address: stakerBtcAddress, - sig: Buffer.from(signedBabylonAddress, "base64"), + sig: Buffer.from(signedBabylonAddress, 'base64'), }); // Encode the BIP322 protobuf message to a Uint8Array btcSig = BIP322Sig.encode(bip322Sig).finish(); } else { // Encode the ECDSA signature to a Uint8Array - btcSig = Buffer.from(signedBabylonAddress, "base64"); + btcSig = Buffer.from(signedBabylonAddress, 'base64'); } return { @@ -1144,19 +1016,13 @@ export class BabylonBtcStakingManager { * @returns The unbonding, slashing, and unbonding slashing transactions and * PSBTs. */ - private async createDelegationTransactionsAndPsbts( - stakingInstance: Staking, - stakingTx: Transaction, - ) { - const { transaction: unbondingTx } = - stakingInstance.createUnbondingTransaction(stakingTx); + private async createDelegationTransactionsAndPsbts(stakingInstance: Staking, stakingTx: Transaction) { + const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction(stakingTx); // Create slashing transactions and extract signatures - const { psbt: slashingPsbt } = - stakingInstance.createStakingOutputSlashingPsbt(stakingTx); + const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingPsbt(stakingTx); - const { psbt: unbondingSlashingPsbt } = - stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx); + const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx); return { unbondingTx, @@ -1182,7 +1048,7 @@ export class BabylonBtcStakingManager { * @returns The protobuf message. */ private async createBtcDelegationMsg( - channel: "delegation:create" | "delegation:register" | "delegation:expand", + channel: 'delegation:create' | 'delegation:register' | 'delegation:expand', stakingInstance: Staking, stakingInput: StakingInputs, stakingTx: Transaction, @@ -1190,28 +1056,27 @@ export class BabylonBtcStakingManager { stakerBtcInfo: StakerInfo, params: StakingParams, options?: { - inclusionProof?: btcstaking.InclusionProof, + inclusionProof?: btcstaking.InclusionProof; delegationExpansionInfo?: { - previousStakingTx: Transaction, - fundingTx: Transaction, - } + previousStakingTx: Transaction; + fundingTx: Transaction; + }; } ): Promise<{ - typeUrl: string, - value: btcstakingtx.MsgCreateBTCDelegation | btcstakingtx.MsgBtcStakeExpand, + typeUrl: string; + value: btcstakingtx.MsgCreateBTCDelegation | btcstakingtx.MsgBtcStakeExpand; }> { if (!params.slashing) { throw new StakingError( StakingErrorCode.INVALID_PARAMS, - "Slashing parameters are required for creating delegation message", + 'Slashing parameters are required for creating delegation message' ); } - const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = - await this.createDelegationTransactionsAndPsbts( - stakingInstance, - stakingTx, - ); + const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = await this.createDelegationTransactionsAndPsbts( + stakingInstance, + stakingTx + ); const slashingContracts: Contract[] = [ { @@ -1252,26 +1117,20 @@ export class BabylonBtcStakingManager { stakingDuration: stakingInput.stakingTimelock, slashingFeeSat: params.slashing.minSlashingTxFeeSat, slashingPkScriptHex: params.slashing.slashingPkScriptHex, - type: "staking-slashing", + type: 'staking-slashing', }); - const signedSlashingPsbtHex = await this.btcProvider.signPsbt( - slashingPsbt.toHex(), - { - contracts: slashingContracts, - action: { - name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, - }, + const signedSlashingPsbtHex = await this.btcProvider.signPsbt(slashingPsbt.toHex(), { + contracts: slashingContracts, + action: { + name: ActionName.SIGN_BTC_SLASHING_TRANSACTION, }, - ); + }); - const signedSlashingTx = Psbt.fromHex( - signedSlashingPsbtHex, - ).extractTransaction(); - const slashingSig = - extractFirstSchnorrSignatureFromTransaction(signedSlashingTx); + const signedSlashingTx = Psbt.fromHex(signedSlashingPsbtHex).extractTransaction(); + const slashingSig = extractFirstSchnorrSignatureFromTransaction(signedSlashingTx); if (!slashingSig) { - throw new Error("No signature found in the staking output slashing PSBT"); + throw new Error('No signature found in the staking output slashing PSBT'); } const unbondingSlashingContracts: Contract[] = [ @@ -1313,88 +1172,61 @@ export class BabylonBtcStakingManager { unbondingFeeSat: params.unbondingFeeSat, slashingFeeSat: params.slashing.minSlashingTxFeeSat, slashingPkScriptHex: params.slashing.slashingPkScriptHex, - type: "unbonding-slashing", + type: 'unbonding-slashing', }); - const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt( - unbondingSlashingPsbt.toHex(), - { - contracts: unbondingSlashingContracts, - action: { - name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, - }, + const signedUnbondingSlashingPsbtHex = await this.btcProvider.signPsbt(unbondingSlashingPsbt.toHex(), { + contracts: unbondingSlashingContracts, + action: { + name: ActionName.SIGN_BTC_UNBONDING_SLASHING_TRANSACTION, }, - ); + }); - const signedUnbondingSlashingTx = Psbt.fromHex( - signedUnbondingSlashingPsbtHex, - ).extractTransaction(); - const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction( - signedUnbondingSlashingTx, - ); + const signedUnbondingSlashingTx = Psbt.fromHex(signedUnbondingSlashingPsbtHex).extractTransaction(); + const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(signedUnbondingSlashingTx); if (!unbondingSignatures) { - throw new Error( - "No signature found in the unbonding output slashing PSBT", - ); + throw new Error('No signature found in the unbonding output slashing PSBT'); } // Create proof of possession - const proofOfPossession = await this.createProofOfPossession( - channel, - bech32Address, - stakerBtcInfo.address, - ); + const proofOfPossession = await this.createProofOfPossession(channel, bech32Address, stakerBtcInfo.address); const commonMsg = { stakerAddr: bech32Address, pop: proofOfPossession, - btcPk: Uint8Array.from( - Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex"), - ), - fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => - Uint8Array.from(Buffer.from(pk, "hex")), - ), + btcPk: Uint8Array.from(Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, 'hex')), + fpBtcPkList: stakingInput.finalityProviderPksNoCoordHex.map((pk) => Uint8Array.from(Buffer.from(pk, 'hex'))), stakingTime: stakingInput.stakingTimelock, stakingValue: stakingInput.stakingAmountSat, stakingTx: Uint8Array.from(stakingTx.toBuffer()), - slashingTx: Uint8Array.from( - Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), - ), + slashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), 'hex')), delegatorSlashingSig: Uint8Array.from(slashingSig), unbondingTime: params.unbondingTime, unbondingTx: Uint8Array.from(unbondingTx.toBuffer()), unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat, - unbondingSlashingTx: Uint8Array.from( - Buffer.from( - clearTxSignatures(signedUnbondingSlashingTx).toHex(), - "hex", - ), - ), + unbondingSlashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedUnbondingSlashingTx).toHex(), 'hex')), delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), - } + }; // If the delegation is an expansion, we use the MsgBtcStakeExpand message if (options?.delegationExpansionInfo) { - const fundingTx = Uint8Array.from( - options.delegationExpansionInfo.fundingTx.toBuffer()); + const fundingTx = Uint8Array.from(options.delegationExpansionInfo.fundingTx.toBuffer()); const msg = btcstakingtx.MsgBtcStakeExpand.fromPartial({ ...commonMsg, - previousStakingTxHash: - options.delegationExpansionInfo.previousStakingTx.getId(), + previousStakingTxHash: options.delegationExpansionInfo.previousStakingTx.getId(), fundingTx, }); return { typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgBtcStakeExpand, value: msg, - } + }; } // Otherwise, it's a new staking delegation - const msg: btcstakingtx.MsgCreateBTCDelegation = - btcstakingtx.MsgCreateBTCDelegation.fromPartial({ - ...commonMsg, - stakingTxInclusionProof: options?.inclusionProof, - }); + const msg: btcstakingtx.MsgCreateBTCDelegation = btcstakingtx.MsgCreateBTCDelegation.fromPartial({ + ...commonMsg, + stakingTxInclusionProof: options?.inclusionProof, + }); return { typeUrl: BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation, @@ -1408,23 +1240,18 @@ export class BabylonBtcStakingManager { * @param inclusionProof - The inclusion proof. * @returns The inclusion proof. */ - private getInclusionProof( - inclusionProof: InclusionProof, - ): btcstaking.InclusionProof { + private getInclusionProof(inclusionProof: InclusionProof): btcstaking.InclusionProof { const { pos, merkle, blockHashHex } = inclusionProof; const proofHex = deriveMerkleProof(merkle); - const hash = reverseBuffer( - Uint8Array.from(Buffer.from(blockHashHex, "hex")), - ); - const inclusionProofKey: btccheckpoint.TransactionKey = - btccheckpoint.TransactionKey.fromPartial({ - index: pos, - hash, - }); + const hash = reverseBuffer(Uint8Array.from(Buffer.from(blockHashHex, 'hex'))); + const inclusionProofKey: btccheckpoint.TransactionKey = btccheckpoint.TransactionKey.fromPartial({ + index: pos, + hash, + }); return btcstaking.InclusionProof.fromPartial({ key: inclusionProofKey, - proof: Uint8Array.from(Buffer.from(proofHex, "hex")), + proof: Uint8Array.from(Buffer.from(proofHex, 'hex')), }); } } @@ -1435,17 +1262,11 @@ export class BabylonBtcStakingManager { * @param unbondingTx - The unbonding transaction * @returns The staker signature */ -export const getUnbondingTxStakerSignature = ( - unbondingTx: Transaction, -): string => { +export const getUnbondingTxStakerSignature = (unbondingTx: Transaction): string => { try { // There is only one input and one output in the unbonding transaction - return unbondingTx.ins[0].witness[0].toString("hex"); + return unbondingTx.ins[0].witness[0].toString('hex'); } catch (error) { - throw StakingError.fromUnknown( - error, - StakingErrorCode.INVALID_INPUT, - "Failed to get staker signature", - ); + throw StakingError.fromUnknown(error, StakingErrorCode.INVALID_INPUT, 'Failed to get staker signature'); } }; From 89ef8ae486544b80d3615f9337e75ba005afb2a9 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Fri, 4 Jul 2025 13:28:49 +0200 Subject: [PATCH 04/10] feat(babylonlabs-io-btc-staking-ts): change createBtcDelegationMsg method to public Make the createBtcDelegationMsg method public to allow access from outside the BabylonBtcStakingManager class. Issue: BTC-2143 Co-authored-by: llm-git --- modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts index f226660953..1295fde19f 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts @@ -1047,7 +1047,7 @@ export class BabylonBtcStakingManager { * delegation expansion. * @returns The protobuf message. */ - private async createBtcDelegationMsg( + public async createBtcDelegationMsg( channel: 'delegation:create' | 'delegation:register' | 'delegation:expand', stakingInstance: Staking, stakingInput: StakingInputs, From db4be592a7e401d1ad245a75d1f970344ca197a0 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 3 Jul 2025 11:01:36 +0200 Subject: [PATCH 05/10] fix(babylonlabs-io-btc-staking-ts): update package metadata for BitGo fork Update package name, version, and dependencies to support BitGo's fork of the babylonlabs BTC staking library. Simplified build configuration and updated Node.js version requirements. Issue: BTC-2143 Co-authored-by: llm-git --- .../package.json | 60 ++++--------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/modules/babylonlabs-io-btc-staking-ts/package.json b/modules/babylonlabs-io-btc-staking-ts/package.json index dcd27b53be..9edd7116c2 100644 --- a/modules/babylonlabs-io-btc-staking-ts/package.json +++ b/modules/babylonlabs-io-btc-staking-ts/package.json @@ -1,21 +1,20 @@ { - "name": "@babylonlabs-io/btc-staking-ts", - "version": "0.0.0-semantic-release", + "name": "@bitgo/babylonlabs-io-btc-staking-ts", + "version": "2.5.7", "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", "typings": "dist/index.d.ts", "type": "module", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, "scripts": { - "generate-types": "dts-bundle-generator -o ./dist/index.d.ts ./src/index.ts", + "generate-types": "dts-bundle-generator -o ./dist/index.d.cts ./src/index.ts --no-check", "build": "node build.js && npm run generate-types", - "format": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", - "format:fix": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", - "lint": "eslint ./src --fix", "prepare": "husky", "prepublishOnly": "npm run build", - "test": "jest --verbose", - "test:watch": "jest --watch", "version:canary": "npm version prerelease --preid=canary" }, "files": [ @@ -28,56 +27,21 @@ "btc-staking" ], "engines": { - "node": ">=22.0.0 <25.0.0" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "src/**/*.ts": [ - "prettier --write", - "eslint --fix" - ], - "tests/**/*.ts": [ - "prettier --write", - "eslint --fix" - ] + "node": ">=18 < 23" }, "author": "Babylon Labs Ltd.", "license": "SEE LICENSE IN LICENSE", "devDependencies": { - "@commitlint/cli": "^19.8.0", - "@commitlint/config-conventional": "^19.8.0", - "@semantic-release/changelog": "^6.0.3", - "@semantic-release/git": "^10.0.1", - "@semantic-release/npm": "^12.0.1", - "@types/jest": "^29.5.12", - "@types/node": "^20.11.30", - "@typescript-eslint/parser": "^7.4.0", "dts-bundle-generator": "^9.3.1", - "ecpair": "^2.1.0", - "esbuild": "^0.25.9", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.1.3", - "husky": "^9.0.11", - "jest": "^29.7.0", - "lint-staged": "^15.2.7", - "nanoevents": "^9.1.0", - "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^3.2.4", - "semantic-release": "^24.2.3", - "ts-jest": "^29.1.4", - "typescript": "^5.4.5", - "typescript-eslint": "^7.4.0" + "esbuild": "^0.20.2", + "nanoevents": "^9.1.0" }, "dependencies": { "@babylonlabs-io/babylon-proto-ts": "1.7.2", "@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3", "@cosmjs/encoding": "^0.33.0", - "bitcoinjs-lib": "6.1.5" + "bip174": "=2.1.1", + "bitcoinjs-lib": "^6.1.7" }, "publishConfig": { "access": "public" From 86d072783ebca743079815e58a6a002e96cd4219 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 3 Jul 2025 11:03:24 +0200 Subject: [PATCH 06/10] fix(babylonlabs-io-btc-staking-ts): add BitGo-specific exports Add exports for babylon and staking utility functions needed by BitGo integration code. Issue: BTC-2143 Co-authored-by: llm-git --- modules/babylonlabs-io-btc-staking-ts/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/babylonlabs-io-btc-staking-ts/src/index.ts b/modules/babylonlabs-io-btc-staking-ts/src/index.ts index f28cccdf56..3f151a34df 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/index.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/index.ts @@ -15,3 +15,7 @@ export { export * from "./utils/utxo/findInputUTXO"; export * from "./utils/utxo/getPsbtInputFields"; export * from "./utils/utxo/getScriptType"; + +// BitGo-specific exports +export * from "./utils/babylon"; +export * from "./utils/staking"; From c8b89ed14b97ecd1c0bf43ec27fdeac56fb22324 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Mon, 7 Jul 2025 13:41:50 +0200 Subject: [PATCH 07/10] feat(babylonlabs-io-btc-staking-ts): add channel param to protobuf message creation Add event channel parameter to the BabylonBtcStakingManager method that creates a protobuf message for BTC delegation. Issue: BTC-2143 Co-authored-by: llm-git --- modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts index 1295fde19f..7eee366df6 100644 --- a/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts +++ b/modules/babylonlabs-io-btc-staking-ts/src/staking/manager.ts @@ -1033,6 +1033,7 @@ export class BabylonBtcStakingManager { /** * Creates a protobuf message for the BTC delegation. + * @param channel - The event channel to emit the message on. * @param stakingInstance - The staking instance. * @param stakingInput - The staking inputs. * @param stakingTx - The staking transaction. From 89d8c3a1e87a029dfd1e69f0da77494be5b83047 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Thu, 3 Jul 2025 11:01:15 +0200 Subject: [PATCH 08/10] fix(babylonlabs-io-btc-staking-ts): simplify README to identify as fork Issue: BTC-2143 Co-authored-by: llm-git --- modules/babylonlabs-io-btc-staking-ts/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 modules/babylonlabs-io-btc-staking-ts/README.md diff --git a/modules/babylonlabs-io-btc-staking-ts/README.md b/modules/babylonlabs-io-btc-staking-ts/README.md new file mode 100644 index 0000000000..6766b67a62 --- /dev/null +++ b/modules/babylonlabs-io-btc-staking-ts/README.md @@ -0,0 +1 @@ +BitGo Fork of https://github.com/babylonlabs-io/btc-staking-ts/tree/v2.3.4 \ No newline at end of file From 615017981865811a26eacf5e8bf838e88e9ad6d0 Mon Sep 17 00:00:00 2001 From: Otto Allmendinger Date: Mon, 7 Jul 2025 11:06:42 +0200 Subject: [PATCH 09/10] feat(babylonlabs-io-btc-staking-ts): add build directory to gitignore Issue: BTC-2143 Co-authored-by: llm-git --- modules/babylonlabs-io-btc-staking-ts/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/babylonlabs-io-btc-staking-ts/.gitignore b/modules/babylonlabs-io-btc-staking-ts/.gitignore index 2f621f05c0..6b0417d7d6 100644 --- a/modules/babylonlabs-io-btc-staking-ts/.gitignore +++ b/modules/babylonlabs-io-btc-staking-ts/.gitignore @@ -206,3 +206,5 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/node,macos,windows *.swp *.swo + +build/ \ No newline at end of file From 62a696564c05ba2351abcc77eb7993959f008fcd Mon Sep 17 00:00:00 2001 From: Ben Kelcher Date: Thu, 2 Oct 2025 10:29:36 -0400 Subject: [PATCH 10/10] feat(utxo-staking): support babylon v2.5.7 changes Ticket: SC-3362 --- .../babylonlabs-io-btc-staking-ts/.gitignore | 2 +- .../build/src/constants/dustSat.d.ts | 1 - .../build/src/constants/dustSat.js | 4 - .../build/src/constants/fee.d.ts | 10 - .../build/src/constants/fee.js | 24 - .../build/src/constants/internalPubkey.d.ts | 1 - .../build/src/constants/internalPubkey.js | 6 - .../build/src/constants/keys.d.ts | 1 - .../build/src/constants/keys.js | 5 - .../build/src/constants/psbt.d.ts | 3 - .../build/src/constants/psbt.js | 9 - .../build/src/constants/registry.d.ts | 3 - .../build/src/constants/registry.js | 6 - .../build/src/constants/transaction.d.ts | 1 - .../build/src/constants/transaction.js | 4 - .../build/src/constants/unbonding.d.ts | 1 - .../build/src/constants/unbonding.js | 6 - .../build/src/error/index.d.ts | 13 - .../build/src/error/index.js | 29 - .../build/src/index.d.ts | 13 - .../build/src/index.js | 35 -- .../build/src/staking/index.d.ts | 117 ---- .../build/src/staking/index.js | 274 --------- .../build/src/staking/manager.d.ts | 246 -------- .../build/src/staking/manager.js | 524 ------------------ .../build/src/staking/observable/index.d.ts | 53 -- .../build/src/staking/observable/index.js | 121 ---- .../observable/observableStakingScript.d.ts | 26 - .../observable/observableStakingScript.js | 60 -- .../build/src/staking/psbt.d.ts | 20 - .../build/src/staking/psbt.js | 113 ---- .../build/src/staking/stakingScript.d.ts | 96 ---- .../build/src/staking/stakingScript.js | 257 --------- .../build/src/staking/transactions.d.ts | 191 ------- .../build/src/staking/transactions.js | 515 ----------------- .../build/src/types/UTXO.d.ts | 9 - .../build/src/types/UTXO.js | 2 - .../build/src/types/covenantSignatures.d.ts | 4 - .../build/src/types/covenantSignatures.js | 2 - .../build/src/types/index.d.ts | 3 - .../build/src/types/index.js | 19 - .../build/src/types/params.d.ts | 34 -- .../build/src/types/params.js | 2 - .../build/src/types/psbtOutputs.d.ts | 16 - .../build/src/types/psbtOutputs.js | 7 - .../build/src/types/transaction.d.ts | 15 - .../build/src/types/transaction.js | 2 - .../build/src/utils/babylon.d.ts | 7 - .../build/src/utils/babylon.js | 20 - .../build/src/utils/btc.d.ts | 48 -- .../build/src/utils/btc.js | 179 ------ .../build/src/utils/fee/index.d.ts | 33 -- .../build/src/utils/fee/index.js | 137 ----- .../build/src/utils/fee/utils.d.ts | 23 - .../build/src/utils/fee/utils.js | 66 --- .../build/src/utils/index.d.ts | 12 - .../build/src/utils/index.js | 31 -- .../build/src/utils/staking/index.d.ts | 116 ---- .../build/src/utils/staking/index.js | 256 --------- .../build/src/utils/staking/param.d.ts | 3 - .../build/src/utils/staking/param.js | 32 -- .../build/src/utils/utxo/findInputUTXO.d.ts | 3 - .../build/src/utils/utxo/findInputUTXO.js | 14 - .../src/utils/utxo/getPsbtInputFields.d.ts | 13 - .../src/utils/utxo/getPsbtInputFields.js | 67 --- .../build/src/utils/utxo/getScriptType.d.ts | 21 - .../build/src/utils/utxo/getScriptType.js | 59 -- .../package.json | 2 +- modules/utxo-staking/package.json | 4 +- .../src/babylon/delegationMessage.ts | 34 +- scripts/vendor-github-repo.ts | 28 +- yarn.lock | 123 +++- 72 files changed, 183 insertions(+), 4053 deletions(-) delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts delete mode 100644 modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js diff --git a/modules/babylonlabs-io-btc-staking-ts/.gitignore b/modules/babylonlabs-io-btc-staking-ts/.gitignore index 6b0417d7d6..f59c3e6ee6 100644 --- a/modules/babylonlabs-io-btc-staking-ts/.gitignore +++ b/modules/babylonlabs-io-btc-staking-ts/.gitignore @@ -207,4 +207,4 @@ $RECYCLE.BIN/ *.swp *.swo -build/ \ No newline at end of file +build/ diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts deleted file mode 100644 index e4f8848e27..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const BTC_DUST_SAT = 546; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js deleted file mode 100644 index 56a8728e7b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/dustSat.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BTC_DUST_SAT = void 0; -exports.BTC_DUST_SAT = 546; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts deleted file mode 100644 index 05adbb74e3..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export declare const DEFAULT_INPUT_SIZE = 180; -export declare const P2WPKH_INPUT_SIZE = 68; -export declare const P2TR_INPUT_SIZE = 58; -export declare const TX_BUFFER_SIZE_OVERHEAD = 11; -export declare const LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30; -export declare const MAX_NON_LEGACY_OUTPUT_SIZE = 43; -export declare const WITHDRAW_TX_BUFFER_SIZE = 17; -export declare const WALLET_RELAY_FEE_RATE_THRESHOLD = 2; -export declare const OP_RETURN_OUTPUT_VALUE_SIZE = 8; -export declare const OP_RETURN_VALUE_SERIALIZE_SIZE = 1; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js deleted file mode 100644 index de3f788be5..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/fee.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.OP_RETURN_VALUE_SERIALIZE_SIZE = exports.OP_RETURN_OUTPUT_VALUE_SIZE = exports.WALLET_RELAY_FEE_RATE_THRESHOLD = exports.WITHDRAW_TX_BUFFER_SIZE = exports.MAX_NON_LEGACY_OUTPUT_SIZE = exports.LOW_RATE_ESTIMATION_ACCURACY_BUFFER = exports.TX_BUFFER_SIZE_OVERHEAD = exports.P2TR_INPUT_SIZE = exports.P2WPKH_INPUT_SIZE = exports.DEFAULT_INPUT_SIZE = void 0; -// Estimated size of a non-SegWit input in bytes -exports.DEFAULT_INPUT_SIZE = 180; -// Estimated size of a P2WPKH input in bytes -exports.P2WPKH_INPUT_SIZE = 68; -// Estimated size of a P2TR input in bytes -exports.P2TR_INPUT_SIZE = 58; -// Estimated size of a transaction buffer in bytes -exports.TX_BUFFER_SIZE_OVERHEAD = 11; -// Buffer for estimation accuracy when fee rate <= 2 sat/byte -exports.LOW_RATE_ESTIMATION_ACCURACY_BUFFER = 30; -// Size of a Taproot output, the largest non-legacy output type -exports.MAX_NON_LEGACY_OUTPUT_SIZE = 43; -// Buffer size for withdraw transaction fee calculation -exports.WITHDRAW_TX_BUFFER_SIZE = 17; -// Threshold for wallet relay fee rate. Different buffer fees are used based on this threshold -exports.WALLET_RELAY_FEE_RATE_THRESHOLD = 2; -// Estimated size of the OP_RETURN output value in bytes -exports.OP_RETURN_OUTPUT_VALUE_SIZE = 8; -// Because our OP_RETURN data will always be less than 80 bytes, which is less than 0xfd (253), -// the value serialization size will always be 1 byte. -exports.OP_RETURN_VALUE_SERIALIZE_SIZE = 1; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts deleted file mode 100644 index e5a7bf8bab..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const internalPubkey: Buffer; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js deleted file mode 100644 index 1205fa77b1..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/internalPubkey.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.internalPubkey = void 0; -// internalPubkey denotes an unspendable internal public key to be used for the taproot output -const key = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; -exports.internalPubkey = Buffer.from(key, "hex").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts deleted file mode 100644 index c292c88b5a..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const NO_COORD_PK_BYTE_LENGTH = 32; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js deleted file mode 100644 index 904a4a15b7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/keys.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NO_COORD_PK_BYTE_LENGTH = void 0; -// NO_COORD_PK_BYTE_LENGTH is the length of a BTC public key without the coordinate in bytes. -exports.NO_COORD_PK_BYTE_LENGTH = 32; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts deleted file mode 100644 index c1fc587929..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare const RBF_SEQUENCE = 4294967293; -export declare const NON_RBF_SEQUENCE = 4294967295; -export declare const TRANSACTION_VERSION = 2; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js deleted file mode 100644 index 64fbdc47e0..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/psbt.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TRANSACTION_VERSION = exports.NON_RBF_SEQUENCE = exports.RBF_SEQUENCE = void 0; -// This sequence enables both the locktime field and also replace-by-fee -exports.RBF_SEQUENCE = 0xfffffffd; -// This sequence means the transaction is not replaceable -exports.NON_RBF_SEQUENCE = 0xffffffff; -// The Transaction version number used across the library(to be set in the psbt) -exports.TRANSACTION_VERSION = 2; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts deleted file mode 100644 index c4e0b5a6b8..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare const BABYLON_REGISTRY_TYPE_URLS: { - MsgCreateBTCDelegation: string; -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js deleted file mode 100644 index d405d58347..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/registry.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BABYLON_REGISTRY_TYPE_URLS = void 0; -exports.BABYLON_REGISTRY_TYPE_URLS = { - MsgCreateBTCDelegation: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts deleted file mode 100644 index a35d33640d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const REDEEM_VERSION = 192; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js deleted file mode 100644 index 1a2bb2937e..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/transaction.js +++ /dev/null @@ -1,4 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.REDEEM_VERSION = void 0; -exports.REDEEM_VERSION = 192; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts deleted file mode 100644 index 3d25a9226e..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare const MIN_UNBONDING_OUTPUT_VALUE = 1000; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js b/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js deleted file mode 100644 index ca71d6622e..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/constants/unbonding.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.MIN_UNBONDING_OUTPUT_VALUE = void 0; -// minimum unbonding output value to avoid the unbonding output value being -// less than Bitcoin dust -exports.MIN_UNBONDING_OUTPUT_VALUE = 1000; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts deleted file mode 100644 index 2ec9884c74..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -export declare enum StakingErrorCode { - UNKNOWN_ERROR = "UNKNOWN_ERROR", - INVALID_INPUT = "INVALID_INPUT", - INVALID_OUTPUT = "INVALID_OUTPUT", - SCRIPT_FAILURE = "SCRIPT_FAILURE", - BUILD_TRANSACTION_FAILURE = "BUILD_TRANSACTION_FAILURE", - INVALID_PARAMS = "INVALID_PARAMS" -} -export declare class StakingError extends Error { - code: StakingErrorCode; - constructor(code: StakingErrorCode, message?: string); - static fromUnknown(error: unknown, code: StakingErrorCode, fallbackMsg?: string): StakingError; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js deleted file mode 100644 index 3c571afd78..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/error/index.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StakingError = exports.StakingErrorCode = void 0; -var StakingErrorCode; -(function (StakingErrorCode) { - StakingErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; - StakingErrorCode["INVALID_INPUT"] = "INVALID_INPUT"; - StakingErrorCode["INVALID_OUTPUT"] = "INVALID_OUTPUT"; - StakingErrorCode["SCRIPT_FAILURE"] = "SCRIPT_FAILURE"; - StakingErrorCode["BUILD_TRANSACTION_FAILURE"] = "BUILD_TRANSACTION_FAILURE"; - StakingErrorCode["INVALID_PARAMS"] = "INVALID_PARAMS"; -})(StakingErrorCode || (exports.StakingErrorCode = StakingErrorCode = {})); -class StakingError extends Error { - constructor(code, message) { - super(message); - this.code = code; - } - // Static method to safely handle unknown errors - static fromUnknown(error, code, fallbackMsg) { - if (error instanceof StakingError) { - return error; - } - if (error instanceof Error) { - return new StakingError(code, error.message); - } - return new StakingError(code, fallbackMsg); - } -} -exports.StakingError = StakingError; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts deleted file mode 100644 index 026c91c5e6..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { StakingScriptData, Staking } from './staking'; -export type { StakingScripts } from './staking'; -export { ObservableStaking, ObservableStakingScriptData } from './staking/observable'; -export * from './staking/transactions'; -export * from './types'; -export * from './utils/btc'; -export * from './utils/babylon'; -export * from './utils/staking'; -export * from './utils/utxo/findInputUTXO'; -export * from './utils/utxo/getPsbtInputFields'; -export * from './utils/utxo/getScriptType'; -export { getBabylonParamByBtcHeight, getBabylonParamByVersion } from './utils/staking/param'; -export * from './staking/manager'; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/index.js deleted file mode 100644 index 02db75a2b8..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/index.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getBabylonParamByVersion = exports.getBabylonParamByBtcHeight = exports.ObservableStakingScriptData = exports.ObservableStaking = exports.Staking = exports.StakingScriptData = void 0; -var staking_1 = require("./staking"); -Object.defineProperty(exports, "StakingScriptData", { enumerable: true, get: function () { return staking_1.StakingScriptData; } }); -Object.defineProperty(exports, "Staking", { enumerable: true, get: function () { return staking_1.Staking; } }); -var observable_1 = require("./staking/observable"); -Object.defineProperty(exports, "ObservableStaking", { enumerable: true, get: function () { return observable_1.ObservableStaking; } }); -Object.defineProperty(exports, "ObservableStakingScriptData", { enumerable: true, get: function () { return observable_1.ObservableStakingScriptData; } }); -__exportStar(require("./staking/transactions"), exports); -__exportStar(require("./types"), exports); -__exportStar(require("./utils/btc"), exports); -__exportStar(require("./utils/babylon"), exports); -__exportStar(require("./utils/staking"), exports); -__exportStar(require("./utils/utxo/findInputUTXO"), exports); -__exportStar(require("./utils/utxo/getPsbtInputFields"), exports); -__exportStar(require("./utils/utxo/getScriptType"), exports); -var param_1 = require("./utils/staking/param"); -Object.defineProperty(exports, "getBabylonParamByBtcHeight", { enumerable: true, get: function () { return param_1.getBabylonParamByBtcHeight; } }); -Object.defineProperty(exports, "getBabylonParamByVersion", { enumerable: true, get: function () { return param_1.getBabylonParamByVersion; } }); -__exportStar(require("./staking/manager"), exports); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts deleted file mode 100644 index c7e95fa536..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.d.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { networks, Psbt, Transaction } from "bitcoinjs-lib"; -import { StakingParams } from "../types/params"; -import { UTXO } from "../types/UTXO"; -import { StakingScripts } from "./stakingScript"; -import { PsbtResult, TransactionResult } from "../types/transaction"; -export * from "./stakingScript"; -export interface StakerInfo { - address: string; - publicKeyNoCoordHex: string; -} -export declare class Staking { - network: networks.Network; - stakerInfo: StakerInfo; - params: StakingParams; - finalityProviderPkNoCoordHex: string; - stakingTimelock: number; - constructor(network: networks.Network, stakerInfo: StakerInfo, params: StakingParams, finalityProviderPkNoCoordHex: string, stakingTimelock: number); - /** - * buildScripts builds the staking scripts for the staking transaction. - * Note: different staking types may have different scripts. - * e.g the observable staking script has a data embed script. - * - * @returns {StakingScripts} - The staking scripts. - */ - buildScripts(): StakingScripts; - /** - * Create a staking transaction for staking. - * - * @param {number} stakingAmountSat - The amount to stake in satoshis. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {TransactionResult} - An object containing the unsigned - * transaction, and fee - * @throws {StakingError} - If the transaction cannot be built - */ - createStakingTransaction(stakingAmountSat: number, inputUTXOs: UTXO[], feeRate: number): TransactionResult; - /** - * Create a staking psbt based on the existing staking transaction. - * - * @param {Transaction} stakingTx - The staking transaction. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. The UTXOs that were used to create the staking transaction should - * be included in this array. - * @returns {Psbt} - The psbt. - */ - toStakingPsbt(stakingTx: Transaction, inputUTXOs: UTXO[]): Psbt; - /** - * Create an unbonding transaction for staking. - * - * @param {Transaction} stakingTx - The staking transaction to unbond. - * @returns {TransactionResult} - An object containing the unsigned - * transaction, and fee - * @throws {StakingError} - If the transaction cannot be built - */ - createUnbondingTransaction(stakingTx: Transaction): TransactionResult; - /** - * Create an unbonding psbt based on the existing unbonding transaction and - * staking transaction. - * - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {Transaction} stakingTx - The staking transaction. - * - * @returns {Psbt} - The psbt. - */ - toUnbondingPsbt(unbondingTx: Transaction, stakingTx: Transaction): Psbt; - /** - * Creates a withdrawal transaction that spends from an unbonding or slashing - * transaction. The timelock on the input transaction must have expired before - * this withdrawal can be valid. - * - * @param {Transaction} earlyUnbondedTx - The unbonding or slashing - * transaction to withdraw from - * @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal - * transaction - * @returns {PsbtResult} - Contains the unsigned PSBT and fee amount - * @throws {StakingError} - If the input transaction is invalid or withdrawal - * transaction cannot be built - */ - createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx: Transaction, feeRate: number): PsbtResult; - /** - * Create a withdrawal psbt that spends a naturally expired staking - * transaction. - * - * @param {Transaction} stakingTx - The staking transaction to withdraw from. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createWithdrawStakingExpiredPsbt(stakingTx: Transaction, feeRate: number): PsbtResult; - /** - * Create a slashing psbt spending from the staking output. - * - * @param {Transaction} stakingTx - The staking transaction to slash. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createStakingOutputSlashingPsbt(stakingTx: Transaction): PsbtResult; - /** - * Create a slashing psbt for an unbonding output. - * - * @param {Transaction} unbondingTx - The unbonding transaction to slash. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createUnbondingOutputSlashingPsbt(unbondingTx: Transaction): PsbtResult; - /** - * Create a withdraw slashing psbt that spends a slashing transaction from the - * staking output. - * - * @param {Transaction} slashingTx - The slashing transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createWithdrawSlashingPsbt(slashingTx: Transaction, feeRate: number): PsbtResult; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js deleted file mode 100644 index 5a48f3f6b7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/index.js +++ /dev/null @@ -1,274 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Staking = void 0; -const stakingScript_1 = require("./stakingScript"); -const error_1 = require("../error"); -const transactions_1 = require("./transactions"); -const btc_1 = require("../utils/btc"); -const staking_1 = require("../utils/staking"); -const staking_2 = require("../utils/staking"); -const psbt_1 = require("./psbt"); -__exportStar(require("./stakingScript"), exports); -class Staking { - constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) { - // Perform validations - if (!(0, btc_1.isValidBitcoinAddress)(stakerInfo.address, network)) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staker bitcoin address"); - } - if (!(0, btc_1.isValidNoCoordPublicKey)(stakerInfo.publicKeyNoCoordHex)) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staker public key"); - } - if (!(0, btc_1.isValidNoCoordPublicKey)(finalityProviderPkNoCoordHex)) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid finality provider public key"); - } - (0, staking_1.validateParams)(params); - (0, staking_1.validateStakingTimelock)(stakingTimelock, params); - this.network = network; - this.stakerInfo = stakerInfo; - this.params = params; - this.finalityProviderPkNoCoordHex = finalityProviderPkNoCoordHex; - this.stakingTimelock = stakingTimelock; - } - /** - * buildScripts builds the staking scripts for the staking transaction. - * Note: different staking types may have different scripts. - * e.g the observable staking script has a data embed script. - * - * @returns {StakingScripts} - The staking scripts. - */ - buildScripts() { - const { covenantQuorum, covenantNoCoordPks, unbondingTime } = this.params; - // Create staking script data - let stakingScriptData; - try { - stakingScriptData = new stakingScript_1.StakingScriptData(Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"), [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")], (0, staking_2.toBuffers)(covenantNoCoordPks), covenantQuorum, this.stakingTimelock, unbondingTime); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking script data"); - } - // Build scripts - let scripts; - try { - scripts = stakingScriptData.buildScripts(); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking scripts"); - } - return scripts; - } - /** - * Create a staking transaction for staking. - * - * @param {number} stakingAmountSat - The amount to stake in satoshis. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {TransactionResult} - An object containing the unsigned - * transaction, and fee - * @throws {StakingError} - If the transaction cannot be built - */ - createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) { - (0, staking_1.validateStakingTxInputData)(stakingAmountSat, this.stakingTimelock, this.params, inputUTXOs, feeRate); - const scripts = this.buildScripts(); - try { - const { transaction, fee } = (0, transactions_1.stakingTransaction)(scripts, stakingAmountSat, this.stakerInfo.address, inputUTXOs, this.network, feeRate); - return { - transaction, - fee, - }; - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned staking transaction"); - } - } - ; - /** - * Create a staking psbt based on the existing staking transaction. - * - * @param {Transaction} stakingTx - The staking transaction. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. The UTXOs that were used to create the staking transaction should - * be included in this array. - * @returns {Psbt} - The psbt. - */ - toStakingPsbt(stakingTx, inputUTXOs) { - // Check the staking output index can be found - const scripts = this.buildScripts(); - const stakingOutputInfo = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); - (0, staking_1.findMatchingTxOutputIndex)(stakingTx, stakingOutputInfo.outputAddress, this.network); - return (0, psbt_1.stakingPsbt)(stakingTx, this.network, inputUTXOs, (0, btc_1.isTaproot)(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : undefined); - } - /** - * Create an unbonding transaction for staking. - * - * @param {Transaction} stakingTx - The staking transaction to unbond. - * @returns {TransactionResult} - An object containing the unsigned - * transaction, and fee - * @throws {StakingError} - If the transaction cannot be built - */ - createUnbondingTransaction(stakingTx) { - // Build scripts - const scripts = this.buildScripts(); - const { outputAddress } = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); - // Reconstruct the stakingOutputIndex - const stakingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(stakingTx, outputAddress, this.network); - // Create the unbonding transaction - try { - const { transaction } = (0, transactions_1.unbondingTransaction)(scripts, stakingTx, this.params.unbondingFeeSat, this.network, stakingOutputIndex); - return { - transaction, - fee: this.params.unbondingFeeSat, - }; - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the unbonding transaction"); - } - } - /** - * Create an unbonding psbt based on the existing unbonding transaction and - * staking transaction. - * - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {Transaction} stakingTx - The staking transaction. - * - * @returns {Psbt} - The psbt. - */ - toUnbondingPsbt(unbondingTx, stakingTx) { - return (0, psbt_1.unbondingPsbt)(this.buildScripts(), unbondingTx, stakingTx, this.network); - } - /** - * Creates a withdrawal transaction that spends from an unbonding or slashing - * transaction. The timelock on the input transaction must have expired before - * this withdrawal can be valid. - * - * @param {Transaction} earlyUnbondedTx - The unbonding or slashing - * transaction to withdraw from - * @param {number} feeRate - Fee rate in satoshis per byte for the withdrawal - * transaction - * @returns {PsbtResult} - Contains the unsigned PSBT and fee amount - * @throws {StakingError} - If the input transaction is invalid or withdrawal - * transaction cannot be built - */ - createWithdrawEarlyUnbondedTransaction(earlyUnbondedTx, feeRate) { - // Build scripts - const scripts = this.buildScripts(); - // Create the withdraw early unbonded transaction - try { - return (0, transactions_1.withdrawEarlyUnbondedTransaction)(scripts, earlyUnbondedTx, this.stakerInfo.address, this.network, feeRate); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned withdraw early unbonded transaction"); - } - } - /** - * Create a withdrawal psbt that spends a naturally expired staking - * transaction. - * - * @param {Transaction} stakingTx - The staking transaction to withdraw from. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createWithdrawStakingExpiredPsbt(stakingTx, feeRate) { - // Build scripts - const scripts = this.buildScripts(); - const { outputAddress } = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); - // Reconstruct the stakingOutputIndex - const stakingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(stakingTx, outputAddress, this.network); - // Create the timelock unbonded transaction - try { - return (0, transactions_1.withdrawTimelockUnbondedTransaction)(scripts, stakingTx, this.stakerInfo.address, this.network, feeRate, stakingOutputIndex); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned timelock unbonded transaction"); - } - } - /** - * Create a slashing psbt spending from the staking output. - * - * @param {Transaction} stakingTx - The staking transaction to slash. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createStakingOutputSlashingPsbt(stakingTx) { - if (!this.params.slashing) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing parameters are missing"); - } - // Build scripts - const scripts = this.buildScripts(); - // create the slash timelock unbonded transaction - try { - const { psbt } = (0, transactions_1.slashTimelockUnbondedTransaction)(scripts, stakingTx, this.params.slashing.slashingPkScriptHex, this.params.slashing.slashingRate, this.params.slashing.minSlashingTxFeeSat, this.network); - return { - psbt, - fee: this.params.slashing.minSlashingTxFeeSat, - }; - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the slash timelock unbonded transaction"); - } - } - /** - * Create a slashing psbt for an unbonding output. - * - * @param {Transaction} unbondingTx - The unbonding transaction to slash. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createUnbondingOutputSlashingPsbt(unbondingTx) { - if (!this.params.slashing) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing parameters are missing"); - } - // Build scripts - const scripts = this.buildScripts(); - // create the slash timelock unbonded transaction - try { - const { psbt } = (0, transactions_1.slashEarlyUnbondedTransaction)(scripts, unbondingTx, this.params.slashing.slashingPkScriptHex, this.params.slashing.slashingRate, this.params.slashing.minSlashingTxFeeSat, this.network); - return { - psbt, - fee: this.params.slashing.minSlashingTxFeeSat, - }; - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build the slash early unbonded transaction"); - } - } - /** - * Create a withdraw slashing psbt that spends a slashing transaction from the - * staking output. - * - * @param {Transaction} slashingTx - The slashing transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} - An object containing the unsigned psbt and fee - * @throws {StakingError} - If the delegation is invalid or the transaction cannot be built - */ - createWithdrawSlashingPsbt(slashingTx, feeRate) { - // Build scripts - const scripts = this.buildScripts(); - const slashingOutputInfo = (0, staking_1.deriveSlashingOutput)(scripts, this.network); - // Reconstruct and validate the slashingOutputIndex - const slashingOutputIndex = (0, staking_1.findMatchingTxOutputIndex)(slashingTx, slashingOutputInfo.outputAddress, this.network); - // Create the withdraw slashed transaction - try { - return (0, transactions_1.withdrawSlashingTransaction)(scripts, slashingTx, this.stakerInfo.address, this.network, feeRate, slashingOutputIndex); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build withdraw slashing transaction"); - } - } -} -exports.Staking = Staking; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts deleted file mode 100644 index 4f9b38ee28..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.d.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { networks, Transaction } from "bitcoinjs-lib"; -import { StakingParams, VersionedStakingParams } from "../types/params"; -import { TransactionResult, UTXO } from "../types"; -import { StakerInfo, Staking } from "."; -import { btcstaking, btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; -import { ProofOfPossessionBTC } from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; -export interface BtcProvider { - signPsbt(signingStep: SigningStep, psbtHex: string): Promise; - signMessage: (signingStep: SigningStep, message: string, type: "ecdsa" | "bip322-simple") => Promise; -} -export interface BabylonProvider { - /** - * Signs a Babylon chain transaction using the provided signing step. - * This is primarily used for signing MsgCreateBTCDelegation transactions - * which register the BTC delegation on the Babylon Genesis chain. - * - * @param {SigningStep} signingStep - The current signing step context - * @param {object} msg - The Cosmos SDK transaction message to sign - * @param {string} msg.typeUrl - The Protobuf type URL identifying the message type - * @param {T} msg.value - The transaction message data matching the typeUrl - * @returns {Promise} The signed transaction bytes - */ - signTransaction: (signingStep: SigningStep, msg: { - typeUrl: string; - value: T; - }) => Promise; -} -export declare enum SigningStep { - STAKING_SLASHING = "staking-slashing", - UNBONDING_SLASHING = "unbonding-slashing", - PROOF_OF_POSSESSION = "proof-of-possession", - CREATE_BTC_DELEGATION_MSG = "create-btc-delegation-msg", - STAKING = "staking", - UNBONDING = "unbonding", - WITHDRAW_STAKING_EXPIRED = "withdraw-staking-expired", - WITHDRAW_EARLY_UNBONDED = "withdraw-early-unbonded", - WITHDRAW_SLASHING = "withdraw-slashing" -} -interface StakingInputs { - finalityProviderPkNoCoordHex: string; - stakingAmountSat: number; - stakingTimelock: number; -} -interface InclusionProof { - pos: number; - merkle: string[]; - blockHashHex: string; -} -export declare class BabylonBtcStakingManager { - protected stakingParams: VersionedStakingParams[]; - protected btcProvider: BtcProvider; - protected network: networks.Network; - protected babylonProvider: BabylonProvider; - constructor(network: networks.Network, stakingParams: VersionedStakingParams[], btcProvider: BtcProvider, babylonProvider: BabylonProvider); - /** - * Creates a signed Pre-Staking Registration transaction that is ready to be - * sent to the Babylon chain. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param babylonBtcTipHeight - The Babylon BTC tip height. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @param babylonAddress - The Babylon bech32 encoded address of the staker. - * @returns The signed babylon pre-staking registration transaction in base64 - * format. - */ - preStakeRegistrationBabylonTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, babylonBtcTipHeight: number, inputUTXOs: UTXO[], feeRate: number, babylonAddress: string): Promise<{ - signedBabylonTx: Uint8Array; - stakingTx: Transaction; - }>; - /** - * Creates a signed post-staking registration transaction that is ready to be - * sent to the Babylon chain. This is used when a staking transaction is - * already created and included in a BTC block and we want to register it on - * the Babylon chain. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingTx - The staking transaction. - * @param stakingTxHeight - The BTC height in which the staking transaction - * is included. - * @param stakingInput - The staking inputs. - * @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction - * inclusion in a Bitcoin block that is k-deep. - * @param babylonAddress - The Babylon bech32 encoded address of the staker. - * @returns The signed babylon transaction in base64 format. - */ - postStakeRegistrationBabylonTransaction(stakerBtcInfo: StakerInfo, stakingTx: Transaction, stakingTxHeight: number, stakingInput: StakingInputs, inclusionProof: InclusionProof, babylonAddress: string): Promise<{ - signedBabylonTx: Uint8Array; - }>; - /** - * Estimates the BTC fee required for staking. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon - * chain. - * @param stakingInput - The staking inputs. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The estimated BTC fee in satoshis. - */ - estimateBtcStakingFee(stakerBtcInfo: StakerInfo, babylonBtcTipHeight: number, stakingInput: StakingInputs, inputUTXOs: UTXO[], feeRate: number): number; - /** - * Creates a signed staking transaction that is ready to be sent to the BTC - * network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param unsignedStakingTx - The unsigned staking transaction. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @returns The signed staking transaction. - */ - createSignedBtcStakingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, unsignedStakingTx: Transaction, inputUTXOs: UTXO[], stakingParamsVersion: number): Promise; - /** - * Creates a partial signed unbonding transaction that is only signed by the - * staker. In order to complete the unbonding transaction, the covenant - * unbonding signatures need to be added to the transaction before sending it - * to the BTC network. - * NOTE: This method should only be used for Babylon phase-1 unbonding. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @returns The partial signed unbonding transaction and its fee. - */ - createPartialSignedBtcUnbondingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction): Promise; - /** - * Creates a signed unbonding transaction that is ready to be sent to the BTC - * network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @param unsignedUnbondingTx - The unsigned unbonding transaction. - * @param covenantUnbondingSignatures - The covenant unbonding signatures. - * It can be retrieved from the Babylon chain or API. - * @returns The signed unbonding transaction and its fee. - */ - createSignedBtcUnbondingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction, unsignedUnbondingTx: Transaction, covenantUnbondingSignatures: { - btcPkHex: string; - sigHex: string; - }[]): Promise; - /** - * Creates a signed withdrawal transaction on the unbodning output expiry path - * that is ready to be sent to the BTC network. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param earlyUnbondingTx - The early unbonding transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, earlyUnbondingTx: Transaction, feeRate: number): Promise; - /** - * Creates a signed withdrawal transaction on the staking output expiry path - * that is ready to be sent to the BTC network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, stakingTx: Transaction, feeRate: number): Promise; - /** - * Creates a signed withdrawal transaction for the expired slashing output that - * is ready to be sent to the BTC network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param slashingTx - The slashing transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo: StakerInfo, stakingInput: StakingInputs, stakingParamsVersion: number, slashingTx: Transaction, feeRate: number): Promise; - /** - * Creates a proof of possession for the staker based on ECDSA signature. - * @param bech32Address - The staker's bech32 address on the babylon chain - * @param stakerBtcAddress - The staker's BTC address. - * @returns The proof of possession. - */ - createProofOfPossession(bech32Address: string, stakerBtcAddress: string): Promise; - /** - * Creates the unbonding, slashing, and unbonding slashing transactions and - * PSBTs. - * @param stakingInstance - The staking instance. - * @param stakingTx - The staking transaction. - * @returns The unbonding, slashing, and unbonding slashing transactions and - * PSBTs. - */ - private createDelegationTransactionsAndPsbts; - /** - * Creates a protobuf message for the BTC delegation. - * @param stakingInstance - The staking instance. - * @param stakingInput - The staking inputs. - * @param stakingTx - The staking transaction. - * @param bech32Address - The staker's babylon chain bech32 address - * @param stakerBtcInfo - The staker's BTC information such as address and - * public key - * @param params - The staking parameters. - * @param inclusionProof - The inclusion proof of the staking transaction. - * @returns The protobuf message. - */ - createBtcDelegationMsg(stakingInstance: Staking, stakingInput: StakingInputs, stakingTx: Transaction, bech32Address: string, stakerBtcInfo: StakerInfo, params: StakingParams, inclusionProof?: btcstaking.InclusionProof): Promise<{ - typeUrl: string; - value: btcstakingtx.MsgCreateBTCDelegation; - }>; - /** - * Gets the inclusion proof for the staking transaction. - * See the type `InclusionProof` for more information - * @param inclusionProof - The inclusion proof. - * @returns The inclusion proof. - */ - private getInclusionProof; -} -/** - * Get the staker signature from the unbonding transaction - * This is used mostly for unbonding transactions from phase-1(Observable) - * @param unbondingTx - The unbonding transaction - * @returns The staker signature - */ -export declare const getUnbondingTxStakerSignature: (unbondingTx: Transaction) => string; -export {}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js deleted file mode 100644 index 4ae5ff10de..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/manager.js +++ /dev/null @@ -1,524 +0,0 @@ -"use strict"; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getUnbondingTxStakerSignature = exports.BabylonBtcStakingManager = exports.SigningStep = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const _1 = require("."); -const babylon_proto_ts_1 = require("@babylonlabs-io/babylon-proto-ts"); -const pop_1 = require("@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"); -const registry_1 = require("../constants/registry"); -const transactions_1 = require("./transactions"); -const param_1 = require("../utils/staking/param"); -const utils_1 = require("../utils"); -const staking_1 = require("../utils/staking"); -const staking_2 = require("../utils/staking"); -const babylon_1 = require("../utils/babylon"); -const error_1 = require("../error"); -const error_2 = require("../error"); -const btc_1 = require("../utils/btc"); -// Event types for the Signing event -var SigningStep; -(function (SigningStep) { - SigningStep["STAKING_SLASHING"] = "staking-slashing"; - SigningStep["UNBONDING_SLASHING"] = "unbonding-slashing"; - SigningStep["PROOF_OF_POSSESSION"] = "proof-of-possession"; - SigningStep["CREATE_BTC_DELEGATION_MSG"] = "create-btc-delegation-msg"; - SigningStep["STAKING"] = "staking"; - SigningStep["UNBONDING"] = "unbonding"; - SigningStep["WITHDRAW_STAKING_EXPIRED"] = "withdraw-staking-expired"; - SigningStep["WITHDRAW_EARLY_UNBONDED"] = "withdraw-early-unbonded"; - SigningStep["WITHDRAW_SLASHING"] = "withdraw-slashing"; -})(SigningStep || (exports.SigningStep = SigningStep = {})); -class BabylonBtcStakingManager { - constructor(network, stakingParams, btcProvider, babylonProvider) { - this.network = network; - this.btcProvider = btcProvider; - this.babylonProvider = babylonProvider; - if (stakingParams.length === 0) { - throw new Error("No staking parameters provided"); - } - this.stakingParams = stakingParams; - } - /** - * Creates a signed Pre-Staking Registration transaction that is ready to be - * sent to the Babylon chain. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param babylonBtcTipHeight - The Babylon BTC tip height. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @param babylonAddress - The Babylon bech32 encoded address of the staker. - * @returns The signed babylon pre-staking registration transaction in base64 - * format. - */ - preStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingInput, babylonBtcTipHeight, inputUTXOs, feeRate, babylonAddress) { - return __awaiter(this, void 0, void 0, function* () { - if (babylonBtcTipHeight === 0) { - throw new Error("Babylon BTC tip height cannot be 0"); - } - if (inputUTXOs.length === 0) { - throw new Error("No input UTXOs provided"); - } - if (!(0, babylon_1.isValidBabylonAddress)(babylonAddress)) { - throw new Error("Invalid Babylon address"); - } - // Get the Babylon params based on the BTC tip height from Babylon chain - const params = (0, param_1.getBabylonParamByBtcHeight)(babylonBtcTipHeight, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - // Create unsigned staking transaction - const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); - // Create delegation message without including inclusion proof - const msg = yield this.createBtcDelegationMsg(staking, stakingInput, transaction, babylonAddress, stakerBtcInfo, params); - return { - signedBabylonTx: yield this.babylonProvider.signTransaction(SigningStep.CREATE_BTC_DELEGATION_MSG, msg), - stakingTx: transaction, - }; - }); - } - /** - * Creates a signed post-staking registration transaction that is ready to be - * sent to the Babylon chain. This is used when a staking transaction is - * already created and included in a BTC block and we want to register it on - * the Babylon chain. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingTx - The staking transaction. - * @param stakingTxHeight - The BTC height in which the staking transaction - * is included. - * @param stakingInput - The staking inputs. - * @param inclusionProof - Merkle Proof of Inclusion: Verifies transaction - * inclusion in a Bitcoin block that is k-deep. - * @param babylonAddress - The Babylon bech32 encoded address of the staker. - * @returns The signed babylon transaction in base64 format. - */ - postStakeRegistrationBabylonTransaction(stakerBtcInfo, stakingTx, stakingTxHeight, stakingInput, inclusionProof, babylonAddress) { - return __awaiter(this, void 0, void 0, function* () { - // Get the Babylon params at the time of the staking transaction - const params = (0, param_1.getBabylonParamByBtcHeight)(stakingTxHeight, this.stakingParams); - if (!(0, babylon_1.isValidBabylonAddress)(babylonAddress)) { - throw new Error("Invalid Babylon address"); - } - const stakingInstance = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - // Validate if the stakingTx is valid based on the retrieved Babylon param - const scripts = stakingInstance.buildScripts(); - const stakingOutputInfo = (0, staking_1.deriveStakingOutputInfo)(scripts, this.network); - // Error will be thrown if the expected staking output address is not found - // in the stakingTx - (0, staking_2.findMatchingTxOutputIndex)(stakingTx, stakingOutputInfo.outputAddress, this.network); - // Create delegation message - const delegationMsg = yield this.createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, babylonAddress, stakerBtcInfo, params, this.getInclusionProof(inclusionProof)); - return { - signedBabylonTx: yield this.babylonProvider.signTransaction(SigningStep.CREATE_BTC_DELEGATION_MSG, delegationMsg), - }; - }); - } - /** - * Estimates the BTC fee required for staking. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param babylonBtcTipHeight - The BTC tip height recorded on the Babylon - * chain. - * @param stakingInput - The staking inputs. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The estimated BTC fee in satoshis. - */ - estimateBtcStakingFee(stakerBtcInfo, babylonBtcTipHeight, stakingInput, inputUTXOs, feeRate) { - if (babylonBtcTipHeight === 0) { - throw new Error("Babylon BTC tip height cannot be 0"); - } - // Get the param based on the tip height - const params = (0, param_1.getBabylonParamByBtcHeight)(babylonBtcTipHeight, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const { fee: stakingFee } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRate); - return stakingFee; - } - /** - * Creates a signed staking transaction that is ready to be sent to the BTC - * network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param unsignedStakingTx - The unsigned staking transaction. - * @param inputUTXOs - The UTXOs that will be used to pay for the staking - * transaction. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @returns The signed staking transaction. - */ - createSignedBtcStakingTransaction(stakerBtcInfo, stakingInput, unsignedStakingTx, inputUTXOs, stakingParamsVersion) { - return __awaiter(this, void 0, void 0, function* () { - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - if (inputUTXOs.length === 0) { - throw new Error("No input UTXOs provided"); - } - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const stakingPsbt = staking.toStakingPsbt(unsignedStakingTx, inputUTXOs); - const signedStakingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.STAKING, stakingPsbt.toHex()); - return bitcoinjs_lib_1.Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); - }); - } - /** - * Creates a partial signed unbonding transaction that is only signed by the - * staker. In order to complete the unbonding transaction, the covenant - * unbonding signatures need to be added to the transaction before sending it - * to the BTC network. - * NOTE: This method should only be used for Babylon phase-1 unbonding. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @returns The partial signed unbonding transaction and its fee. - */ - createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx) { - return __awaiter(this, void 0, void 0, function* () { - // Get the staking params at the time of the staking transaction - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const { transaction: unbondingTx, fee, } = staking.createUnbondingTransaction(stakingTx); - const psbt = staking.toUnbondingPsbt(unbondingTx, stakingTx); - const signedUnbondingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.UNBONDING, psbt.toHex()); - const signedUnbondingTx = bitcoinjs_lib_1.Psbt.fromHex(signedUnbondingPsbtHex).extractTransaction(); - return { - transaction: signedUnbondingTx, - fee, - }; - }); - } - /** - * Creates a signed unbonding transaction that is ready to be sent to the BTC - * network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @param unsignedUnbondingTx - The unsigned unbonding transaction. - * @param covenantUnbondingSignatures - The covenant unbonding signatures. - * It can be retrieved from the Babylon chain or API. - * @returns The signed unbonding transaction and its fee. - */ - createSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, unsignedUnbondingTx, covenantUnbondingSignatures) { - return __awaiter(this, void 0, void 0, function* () { - // Get the staking params at the time of the staking transaction - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - const { transaction: signedUnbondingTx, fee, } = yield this.createPartialSignedBtcUnbondingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx); - // Check the computed txid of the signed unbonding transaction is the same as - // the txid of the unsigned unbonding transaction - if (signedUnbondingTx.getId() !== unsignedUnbondingTx.getId()) { - throw new Error("Unbonding transaction hash does not match the computed hash"); - } - // Add covenant unbonding signatures - // Convert the params of covenants to buffer - const covenantBuffers = params.covenantNoCoordPks.map((covenant) => Buffer.from(covenant, "hex")); - const witness = (0, transactions_1.createCovenantWitness)( - // Since unbonding transactions always have a single input and output, - // we expect exactly one signature in TaprootScriptSpendSig when the - // signing is successful - signedUnbondingTx.ins[0].witness, covenantBuffers, covenantUnbondingSignatures, params.covenantQuorum); - // Overwrite the witness to include the covenant unbonding signatures - signedUnbondingTx.ins[0].witness = witness; - return { - transaction: signedUnbondingTx, - fee, - }; - }); - } - /** - * Creates a signed withdrawal transaction on the unbodning output expiry path - * that is ready to be sent to the BTC network. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param earlyUnbondingTx - The early unbonding transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawEarlyUnbondedTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, earlyUnbondingTx, feeRate) { - return __awaiter(this, void 0, void 0, function* () { - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const { psbt: unbondingPsbt, fee } = staking.createWithdrawEarlyUnbondedTransaction(earlyUnbondingTx, feeRate); - const signedWithdrawalPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_EARLY_UNBONDED, unbondingPsbt.toHex()); - return { - transaction: bitcoinjs_lib_1.Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), - fee, - }; - }); - } - /** - * Creates a signed withdrawal transaction on the staking output expiry path - * that is ready to be sent to the BTC network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param stakingTx - The staking transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawStakingExpiredTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, stakingTx, feeRate) { - return __awaiter(this, void 0, void 0, function* () { - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const { psbt, fee } = staking.createWithdrawStakingExpiredPsbt(stakingTx, feeRate); - const signedWithdrawalPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_STAKING_EXPIRED, psbt.toHex()); - return { - transaction: bitcoinjs_lib_1.Psbt.fromHex(signedWithdrawalPsbtHex).extractTransaction(), - fee, - }; - }); - } - /** - * Creates a signed withdrawal transaction for the expired slashing output that - * is ready to be sent to the BTC network. - * @param stakerBtcInfo - The staker BTC info which includes the BTC address - * and the no-coord public key in hex format. - * @param stakingInput - The staking inputs. - * @param stakingParamsVersion - The params version that was used to create the - * delegation in Babylon chain - * @param slashingTx - The slashing transaction. - * @param feeRate - The fee rate in satoshis per byte. Typical value for the - * fee rate is above 1. If the fee rate is too low, the transaction will not - * be included in a block. - * @returns The signed withdrawal transaction and its fee. - */ - createSignedBtcWithdrawSlashingTransaction(stakerBtcInfo, stakingInput, stakingParamsVersion, slashingTx, feeRate) { - return __awaiter(this, void 0, void 0, function* () { - const params = (0, param_1.getBabylonParamByVersion)(stakingParamsVersion, this.stakingParams); - const staking = new _1.Staking(this.network, stakerBtcInfo, params, stakingInput.finalityProviderPkNoCoordHex, stakingInput.stakingTimelock); - const { psbt, fee } = staking.createWithdrawSlashingPsbt(slashingTx, feeRate); - const signedSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.WITHDRAW_SLASHING, psbt.toHex()); - return { - transaction: bitcoinjs_lib_1.Psbt.fromHex(signedSlashingPsbtHex).extractTransaction(), - fee, - }; - }); - } - /** - * Creates a proof of possession for the staker based on ECDSA signature. - * @param bech32Address - The staker's bech32 address on the babylon chain - * @param stakerBtcAddress - The staker's BTC address. - * @returns The proof of possession. - */ - createProofOfPossession(bech32Address, stakerBtcAddress) { - return __awaiter(this, void 0, void 0, function* () { - let sigType = pop_1.BTCSigType.ECDSA; - // For Taproot or Native SegWit addresses, use the BIP322 signature scheme - // in the proof of possession as it uses the same signature type as the regular - // input UTXO spend. For legacy addresses, use the ECDSA signature scheme. - if ((0, btc_1.isTaproot)(stakerBtcAddress, this.network) - || (0, btc_1.isNativeSegwit)(stakerBtcAddress, this.network)) { - sigType = pop_1.BTCSigType.BIP322; - } - const signedBabylonAddress = yield this.btcProvider.signMessage(SigningStep.PROOF_OF_POSSESSION, bech32Address, sigType === pop_1.BTCSigType.BIP322 ? "bip322-simple" : "ecdsa"); - let btcSig; - if (sigType === pop_1.BTCSigType.BIP322) { - const bip322Sig = pop_1.BIP322Sig.fromPartial({ - address: stakerBtcAddress, - sig: Buffer.from(signedBabylonAddress, "base64"), - }); - // Encode the BIP322 protobuf message to a Uint8Array - btcSig = pop_1.BIP322Sig.encode(bip322Sig).finish(); - } - else { - // Encode the ECDSA signature to a Uint8Array - btcSig = Buffer.from(signedBabylonAddress, "base64"); - } - return { - btcSigType: sigType, - btcSig - }; - }); - } - /** - * Creates the unbonding, slashing, and unbonding slashing transactions and - * PSBTs. - * @param stakingInstance - The staking instance. - * @param stakingTx - The staking transaction. - * @returns The unbonding, slashing, and unbonding slashing transactions and - * PSBTs. - */ - createDelegationTransactionsAndPsbts(stakingInstance, stakingTx) { - return __awaiter(this, void 0, void 0, function* () { - const { transaction: unbondingTx } = stakingInstance.createUnbondingTransaction(stakingTx); - // Create slashing transactions and extract signatures - const { psbt: slashingPsbt } = stakingInstance.createStakingOutputSlashingPsbt(stakingTx); - const { psbt: unbondingSlashingPsbt } = stakingInstance.createUnbondingOutputSlashingPsbt(unbondingTx); - return { - unbondingTx, - slashingPsbt, - unbondingSlashingPsbt, - }; - }); - } - /** - * Creates a protobuf message for the BTC delegation. - * @param stakingInstance - The staking instance. - * @param stakingInput - The staking inputs. - * @param stakingTx - The staking transaction. - * @param bech32Address - The staker's babylon chain bech32 address - * @param stakerBtcInfo - The staker's BTC information such as address and - * public key - * @param params - The staking parameters. - * @param inclusionProof - The inclusion proof of the staking transaction. - * @returns The protobuf message. - */ - createBtcDelegationMsg(stakingInstance, stakingInput, stakingTx, bech32Address, stakerBtcInfo, params, inclusionProof) { - return __awaiter(this, void 0, void 0, function* () { - const { unbondingTx, slashingPsbt, unbondingSlashingPsbt } = yield this.createDelegationTransactionsAndPsbts(stakingInstance, stakingTx); - // Sign the slashing PSBT - const signedSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.STAKING_SLASHING, slashingPsbt.toHex()); - const signedSlashingTx = bitcoinjs_lib_1.Psbt.fromHex(signedSlashingPsbtHex).extractTransaction(); - const slashingSig = extractFirstSchnorrSignatureFromTransaction(signedSlashingTx); - if (!slashingSig) { - throw new Error("No signature found in the staking output slashing PSBT"); - } - // Sign the unbonding slashing PSBT - const signedUnbondingSlashingPsbtHex = yield this.btcProvider.signPsbt(SigningStep.UNBONDING_SLASHING, unbondingSlashingPsbt.toHex()); - const signedUnbondingSlashingTx = bitcoinjs_lib_1.Psbt.fromHex(signedUnbondingSlashingPsbtHex).extractTransaction(); - const unbondingSignatures = extractFirstSchnorrSignatureFromTransaction(signedUnbondingSlashingTx); - if (!unbondingSignatures) { - throw new Error("No signature found in the unbonding output slashing PSBT"); - } - // Create proof of possession - const proofOfPossession = yield this.createProofOfPossession(bech32Address, stakerBtcInfo.address); - // Prepare the final protobuf message - const msg = babylon_proto_ts_1.btcstakingtx.MsgCreateBTCDelegation.fromPartial({ - stakerAddr: bech32Address, - pop: proofOfPossession, - btcPk: Uint8Array.from(Buffer.from(stakerBtcInfo.publicKeyNoCoordHex, "hex")), - fpBtcPkList: [ - Uint8Array.from(Buffer.from(stakingInput.finalityProviderPkNoCoordHex, "hex")), - ], - stakingTime: stakingInput.stakingTimelock, - stakingValue: stakingInput.stakingAmountSat, - stakingTx: Uint8Array.from(stakingTx.toBuffer()), - slashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex")), - delegatorSlashingSig: Uint8Array.from(slashingSig), - unbondingTime: params.unbondingTime, - unbondingTx: Uint8Array.from(unbondingTx.toBuffer()), - unbondingValue: stakingInput.stakingAmountSat - params.unbondingFeeSat, - unbondingSlashingTx: Uint8Array.from(Buffer.from(clearTxSignatures(signedUnbondingSlashingTx).toHex(), "hex")), - delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), - stakingTxInclusionProof: inclusionProof, - }); - return { - typeUrl: registry_1.BABYLON_REGISTRY_TYPE_URLS.MsgCreateBTCDelegation, - value: msg, - }; - }); - } - ; - /** - * Gets the inclusion proof for the staking transaction. - * See the type `InclusionProof` for more information - * @param inclusionProof - The inclusion proof. - * @returns The inclusion proof. - */ - getInclusionProof(inclusionProof) { - const { pos, merkle, blockHashHex } = inclusionProof; - const proofHex = deriveMerkleProof(merkle); - const hash = (0, utils_1.reverseBuffer)(Uint8Array.from(Buffer.from(blockHashHex, "hex"))); - const inclusionProofKey = babylon_proto_ts_1.btccheckpoint.TransactionKey.fromPartial({ - index: pos, - hash, - }); - return babylon_proto_ts_1.btcstaking.InclusionProof.fromPartial({ - key: inclusionProofKey, - proof: Uint8Array.from(Buffer.from(proofHex, "hex")), - }); - } - ; -} -exports.BabylonBtcStakingManager = BabylonBtcStakingManager; -/** - * Extracts the first valid Schnorr signature from a signed transaction. - * - * Since we only handle transactions with a single input and request a signature - * for one public key, there can be at most one signature from the Bitcoin node. - * A valid Schnorr signature is exactly 64 bytes in length. - * - * @param singedTransaction - The signed Bitcoin transaction to extract the signature from - * @returns The first valid 64-byte Schnorr signature found in the transaction witness data, - * or undefined if no valid signature exists - */ -const extractFirstSchnorrSignatureFromTransaction = (singedTransaction) => { - // Loop through each input to extract the witness signature - for (const input of singedTransaction.ins) { - if (input.witness && input.witness.length > 0) { - const schnorrSignature = input.witness[0]; - // Check that it's a 64-byte Schnorr signature - if (schnorrSignature.length === 64) { - return schnorrSignature; // Return the first valid signature found - } - } - } - return undefined; -}; -/** - * Strips all signatures from a transaction by clearing both the script and - * witness data. This is due to the fact that we only need the raw unsigned - * transaction structure. The signatures are sent in a separate protobuf field - * when creating the delegation message in the Babylon. - * @param tx - The transaction to strip signatures from - * @returns A copy of the transaction with all signatures removed - */ -const clearTxSignatures = (tx) => { - tx.ins.forEach((input) => { - input.script = Buffer.alloc(0); - input.witness = []; - }); - return tx; -}; -/** - * Derives the merkle proof from the list of hex strings. Note the - * sibling hashes are reversed from hex before concatenation. - * @param merkle - The merkle proof hex strings. - * @returns The merkle proof in hex string format. - */ -const deriveMerkleProof = (merkle) => { - const proofHex = merkle.reduce((acc, m) => { - return acc + Buffer.from(m, "hex").reverse().toString("hex"); - }, ""); - return proofHex; -}; -/** - * Get the staker signature from the unbonding transaction - * This is used mostly for unbonding transactions from phase-1(Observable) - * @param unbondingTx - The unbonding transaction - * @returns The staker signature - */ -const getUnbondingTxStakerSignature = (unbondingTx) => { - try { - // There is only one input and one output in the unbonding transaction - return unbondingTx.ins[0].witness[0].toString("hex"); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_2.StakingErrorCode.INVALID_INPUT, "Failed to get staker signature"); - } -}; -exports.getUnbondingTxStakerSignature = getUnbondingTxStakerSignature; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts deleted file mode 100644 index 9a26613415..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ObservableVersionedStakingParams } from "../../types/params"; -import { UTXO } from "../../types/UTXO"; -import { TransactionResult } from "../../types/transaction"; -import { ObservableStakingScripts } from "./observableStakingScript"; -import { StakerInfo, Staking } from ".."; -import { networks, Psbt, Transaction } from "bitcoinjs-lib"; -export * from "./observableStakingScript"; -/** - * ObservableStaking is a class that provides an interface to create observable - * staking transactions for the Babylon Staking protocol. - * - * The class requires a network and staker information to create staking - * transactions. - * The staker information includes the staker's address and - * public key(without coordinates). - */ -export declare class ObservableStaking extends Staking { - params: ObservableVersionedStakingParams; - constructor(network: networks.Network, stakerInfo: StakerInfo, params: ObservableVersionedStakingParams, finalityProviderPkNoCoordHex: string, stakingTimelock: number); - /** - * Build the staking scripts for observable staking. - * This method overwrites the base method to include the OP_RETURN tag based - * on the tag provided in the parameters. - * - * @returns {ObservableStakingScripts} - The staking scripts for observable staking. - * @throws {StakingError} - If the scripts cannot be built. - */ - buildScripts(): ObservableStakingScripts; - /** - * Create a staking transaction for observable staking. - * This overwrites the method from the Staking class with the addtion - * of the - * 1. OP_RETURN tag in the staking scripts - * 2. lockHeight parameter - * - * @param {number} stakingAmountSat - The amount to stake in satoshis. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {TransactionResult} - An object containing the unsigned transaction, - * and fee - */ - createStakingTransaction(stakingAmountSat: number, inputUTXOs: UTXO[], feeRate: number): TransactionResult; - /** - * Create a staking psbt for observable staking. - * - * @param {Transaction} stakingTx - The staking transaction. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @returns {Psbt} - The psbt. - */ - toStakingPsbt(stakingTx: Transaction, inputUTXOs: UTXO[]): Psbt; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js deleted file mode 100644 index 79f749f140..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/index.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ObservableStaking = void 0; -const error_1 = require("../../error"); -const transactions_1 = require("../transactions"); -const btc_1 = require("../../utils/btc"); -const staking_1 = require("../../utils/staking"); -const observableStakingScript_1 = require("./observableStakingScript"); -const __1 = require(".."); -const psbt_1 = require("../psbt"); -__exportStar(require("./observableStakingScript"), exports); -/** - * ObservableStaking is a class that provides an interface to create observable - * staking transactions for the Babylon Staking protocol. - * - * The class requires a network and staker information to create staking - * transactions. - * The staker information includes the staker's address and - * public key(without coordinates). - */ -class ObservableStaking extends __1.Staking { - constructor(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock) { - super(network, stakerInfo, params, finalityProviderPkNoCoordHex, stakingTimelock); - if (!params.tag) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Observable staking parameters must include tag"); - } - if (!params.btcActivationHeight) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Observable staking parameters must include a positive activation height"); - } - // Override the staking parameters type to ObservableStakingParams - this.params = params; - } - /** - * Build the staking scripts for observable staking. - * This method overwrites the base method to include the OP_RETURN tag based - * on the tag provided in the parameters. - * - * @returns {ObservableStakingScripts} - The staking scripts for observable staking. - * @throws {StakingError} - If the scripts cannot be built. - */ - buildScripts() { - const { covenantQuorum, covenantNoCoordPks, unbondingTime, tag } = this.params; - // Create staking script data - let stakingScriptData; - try { - stakingScriptData = new observableStakingScript_1.ObservableStakingScriptData(Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex"), [Buffer.from(this.finalityProviderPkNoCoordHex, "hex")], (0, staking_1.toBuffers)(covenantNoCoordPks), covenantQuorum, this.stakingTimelock, unbondingTime, Buffer.from(tag, "hex")); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking script data"); - } - // Build scripts - let scripts; - try { - scripts = stakingScriptData.buildScripts(); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.SCRIPT_FAILURE, "Cannot build staking scripts"); - } - return scripts; - } - /** - * Create a staking transaction for observable staking. - * This overwrites the method from the Staking class with the addtion - * of the - * 1. OP_RETURN tag in the staking scripts - * 2. lockHeight parameter - * - * @param {number} stakingAmountSat - The amount to stake in satoshis. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {TransactionResult} - An object containing the unsigned transaction, - * and fee - */ - createStakingTransaction(stakingAmountSat, inputUTXOs, feeRate) { - (0, staking_1.validateStakingTxInputData)(stakingAmountSat, this.stakingTimelock, this.params, inputUTXOs, feeRate); - const scripts = this.buildScripts(); - // Create the staking transaction - try { - const { transaction, fee } = (0, transactions_1.stakingTransaction)(scripts, stakingAmountSat, this.stakerInfo.address, inputUTXOs, this.network, feeRate, - // `lockHeight` is exclusive of the provided value. - // For example, if a Bitcoin height of X is provided, - // the transaction will be included starting from height X+1. - // https://learnmeabitcoin.com/technical/transaction/locktime/ - this.params.btcActivationHeight - 1); - return { - transaction, - fee, - }; - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.BUILD_TRANSACTION_FAILURE, "Cannot build unsigned staking transaction"); - } - } - /** - * Create a staking psbt for observable staking. - * - * @param {Transaction} stakingTx - The staking transaction. - * @param {UTXO[]} inputUTXOs - The UTXOs to use as inputs for the staking - * transaction. - * @returns {Psbt} - The psbt. - */ - toStakingPsbt(stakingTx, inputUTXOs) { - return (0, psbt_1.stakingPsbt)(stakingTx, this.network, inputUTXOs, (0, btc_1.isTaproot)(this.stakerInfo.address, this.network) ? Buffer.from(this.stakerInfo.publicKeyNoCoordHex, "hex") : undefined); - } -} -exports.ObservableStaking = ObservableStaking; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts deleted file mode 100644 index 908669a846..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { StakingScriptData, StakingScripts } from "../stakingScript"; -export interface ObservableStakingScripts extends StakingScripts { - dataEmbedScript: Buffer; -} -export declare class ObservableStakingScriptData extends StakingScriptData { - magicBytes: Buffer; - constructor(stakerKey: Buffer, finalityProviderKeys: Buffer[], covenantKeys: Buffer[], covenantThreshold: number, stakingTimelock: number, unbondingTimelock: number, magicBytes: Buffer); - /** - * Builds a data embed script for staking in the form: - * OP_RETURN || - * where serializedStakingData is the concatenation of: - * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock - * Note: Only a single finality provider key is supported for now in phase 1 - * @throws {Error} If the number of finality provider keys is not equal to 1. - * @returns {Buffer} The compiled data embed script. - */ - buildDataEmbedScript(): Buffer; - /** - * Builds the staking scripts. - * @returns {ObservableStakingScripts} The staking scripts that can be used to stake. - * contains the timelockScript, unbondingScript, slashingScript, - * unbondingTimelockScript, and dataEmbedScript. - * @throws {Error} If script data is invalid. - */ - buildScripts(): ObservableStakingScripts; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js deleted file mode 100644 index 507ff4edc7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/observable/observableStakingScript.js +++ /dev/null @@ -1,60 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ObservableStakingScriptData = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const stakingScript_1 = require("../stakingScript"); -class ObservableStakingScriptData extends stakingScript_1.StakingScriptData { - constructor(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock, magicBytes) { - super(stakerKey, finalityProviderKeys, covenantKeys, covenantThreshold, stakingTimelock, unbondingTimelock); - if (!magicBytes) { - throw new Error("Missing required input values"); - } - // check that the magic bytes are 4 in length - if (magicBytes.length != stakingScript_1.MAGIC_BYTES_LEN) { - throw new Error("Invalid script data provided"); - } - this.magicBytes = magicBytes; - } - /** - * Builds a data embed script for staking in the form: - * OP_RETURN || - * where serializedStakingData is the concatenation of: - * MagicBytes || Version || StakerPublicKey || FinalityProviderPublicKey || StakingTimeLock - * Note: Only a single finality provider key is supported for now in phase 1 - * @throws {Error} If the number of finality provider keys is not equal to 1. - * @returns {Buffer} The compiled data embed script. - */ - buildDataEmbedScript() { - // Only accept a single finality provider key for now - if (this.finalityProviderKeys.length != 1) { - throw new Error("Only a single finality provider key is supported"); - } - // 1 byte for version - const version = Buffer.alloc(1); - version.writeUInt8(0); - // 2 bytes for staking time - const stakingTimeLock = Buffer.alloc(2); - // big endian - stakingTimeLock.writeUInt16BE(this.stakingTimeLock); - const serializedStakingData = Buffer.concat([ - this.magicBytes, - version, - this.stakerKey, - this.finalityProviderKeys[0], - stakingTimeLock, - ]); - return bitcoinjs_lib_1.script.compile([bitcoinjs_lib_1.opcodes.OP_RETURN, serializedStakingData]); - } - /** - * Builds the staking scripts. - * @returns {ObservableStakingScripts} The staking scripts that can be used to stake. - * contains the timelockScript, unbondingScript, slashingScript, - * unbondingTimelockScript, and dataEmbedScript. - * @throws {Error} If script data is invalid. - */ - buildScripts() { - const scripts = super.buildScripts(); - return Object.assign(Object.assign({}, scripts), { dataEmbedScript: this.buildDataEmbedScript() }); - } -} -exports.ObservableStakingScriptData = ObservableStakingScriptData; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts deleted file mode 100644 index c033fb8d22..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; -import { UTXO } from "../types/UTXO"; -/** - * Convert a staking transaction to a PSBT. - * - * @param {Transaction} stakingTx - The staking transaction to convert to PSBT. - * @param {networks.Network} network - The network to use for the PSBT. - * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking - * transaction. - * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional) - * @returns {Psbt} - The PSBT for the staking transaction. - * @throws {Error} If unable to create PSBT from transaction - */ -export declare const stakingPsbt: (stakingTx: Transaction, network: networks.Network, inputUTXOs: UTXO[], publicKeyNoCoord?: Buffer) => Psbt; -export declare const unbondingPsbt: (scripts: { - unbondingScript: Buffer; - timelockScript: Buffer; - slashingScript: Buffer; - unbondingTimelockScript: Buffer; -}, unbondingTx: Transaction, stakingTx: Transaction, network: networks.Network) => Psbt; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js deleted file mode 100644 index 8c0c34b3e5..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/psbt.js +++ /dev/null @@ -1,113 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.unbondingPsbt = exports.stakingPsbt = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const internalPubkey_1 = require("../constants/internalPubkey"); -const keys_1 = require("../constants/keys"); -const transaction_1 = require("../constants/transaction"); -const staking_1 = require("../utils/staking"); -const findInputUTXO_1 = require("../utils/utxo/findInputUTXO"); -const getPsbtInputFields_1 = require("../utils/utxo/getPsbtInputFields"); -/** - * Convert a staking transaction to a PSBT. - * - * @param {Transaction} stakingTx - The staking transaction to convert to PSBT. - * @param {networks.Network} network - The network to use for the PSBT. - * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking - * transaction. - * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional) - * @returns {Psbt} - The PSBT for the staking transaction. - * @throws {Error} If unable to create PSBT from transaction - */ -const stakingPsbt = (stakingTx, network, inputUTXOs, publicKeyNoCoord) => { - if (publicKeyNoCoord && publicKeyNoCoord.length !== keys_1.NO_COORD_PK_BYTE_LENGTH) { - throw new Error("Invalid public key"); - } - const psbt = new bitcoinjs_lib_1.Psbt({ network }); - if (stakingTx.version !== undefined) - psbt.setVersion(stakingTx.version); - if (stakingTx.locktime !== undefined) - psbt.setLocktime(stakingTx.locktime); - stakingTx.ins.forEach((input) => { - const inputUTXO = (0, findInputUTXO_1.findInputUTXO)(inputUTXOs, input); - const psbtInputData = (0, getPsbtInputFields_1.getPsbtInputFields)(inputUTXO, publicKeyNoCoord); - psbt.addInput(Object.assign({ hash: input.hash, index: input.index, sequence: input.sequence }, psbtInputData)); - }); - stakingTx.outs.forEach((o) => { - psbt.addOutput({ script: o.script, value: o.value }); - }); - return psbt; -}; -exports.stakingPsbt = stakingPsbt; -const unbondingPsbt = (scripts, unbondingTx, stakingTx, network) => { - if (unbondingTx.outs.length !== 1) { - throw new Error("Unbonding transaction must have exactly one output"); - } - if (unbondingTx.ins.length !== 1) { - throw new Error("Unbonding transaction must have exactly one input"); - } - validateUnbondingOutput(scripts, unbondingTx, network); - const psbt = new bitcoinjs_lib_1.Psbt({ network }); - if (unbondingTx.version !== undefined) { - psbt.setVersion(unbondingTx.version); - } - if (unbondingTx.locktime !== undefined) { - psbt.setLocktime(unbondingTx.locktime); - } - const input = unbondingTx.ins[0]; - const outputIndex = input.index; - // Build input tapleaf script - const inputScriptTree = [ - { output: scripts.slashingScript }, - [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], - ]; - // This is the tapleaf we are actually spending - const inputRedeem = { - output: scripts.unbondingScript, - redeemVersion: transaction_1.REDEEM_VERSION, - }; - // Create a P2TR payment that includes scriptTree + redeem - const p2tr = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree: inputScriptTree, - redeem: inputRedeem, - network, - }); - const inputTapLeafScript = { - leafVersion: inputRedeem.redeemVersion, - script: inputRedeem.output, - controlBlock: p2tr.witness[p2tr.witness.length - 1], - }; - psbt.addInput({ - hash: input.hash, - index: input.index, - sequence: input.sequence, - tapInternalKey: internalPubkey_1.internalPubkey, - witnessUtxo: { - value: stakingTx.outs[outputIndex].value, - script: stakingTx.outs[outputIndex].script, - }, - tapLeafScript: [inputTapLeafScript], - }); - psbt.addOutput({ - script: unbondingTx.outs[0].script, - value: unbondingTx.outs[0].value, - }); - return psbt; -}; -exports.unbondingPsbt = unbondingPsbt; -/** - * Validate the unbonding output for a given unbonding transaction. - * - * @param {Object} scripts - The scripts to use for the unbonding output. - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {networks.Network} network - The network to use for the unbonding output. - */ -const validateUnbondingOutput = (scripts, unbondingTx, network) => { - const unbondingOutputInfo = (0, staking_1.deriveUnbondingOutputInfo)(scripts, network); - if (unbondingOutputInfo.scriptPubKey.toString("hex") !== - unbondingTx.outs[0].script.toString("hex")) { - throw new Error("Unbonding output script does not match the expected" + - " script while building psbt"); - } -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts deleted file mode 100644 index 1eb07d97ca..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.d.ts +++ /dev/null @@ -1,96 +0,0 @@ -export declare const MAGIC_BYTES_LEN = 4; -export interface StakingScripts { - timelockScript: Buffer; - unbondingScript: Buffer; - slashingScript: Buffer; - unbondingTimelockScript: Buffer; -} -export declare class StakingScriptData { - stakerKey: Buffer; - finalityProviderKeys: Buffer[]; - covenantKeys: Buffer[]; - covenantThreshold: number; - stakingTimeLock: number; - unbondingTimeLock: number; - constructor(stakerKey: Buffer, finalityProviderKeys: Buffer[], covenantKeys: Buffer[], covenantThreshold: number, stakingTimelock: number, unbondingTimelock: number); - /** - * Validates the staking script. - * @returns {boolean} Returns true if the staking script is valid, otherwise false. - */ - validate(): boolean; - /** - * Builds a timelock script. - * @param timelock - The timelock value to encode in the script. - * @returns {Buffer} containing the compiled timelock script. - */ - buildTimelockScript(timelock: number): Buffer; - /** - * Builds the staking timelock script. - * Only holder of private key for given pubKey can spend after relative lock time - * Creates the timelock script in the form: - * - * OP_CHECKSIGVERIFY - * - * OP_CHECKSEQUENCEVERIFY - * @returns {Buffer} The staking timelock script. - */ - buildStakingTimelockScript(): Buffer; - /** - * Builds the unbonding timelock script. - * Creates the unbonding timelock script in the form: - * - * OP_CHECKSIGVERIFY - * - * OP_CHECKSEQUENCEVERIFY - * @returns {Buffer} The unbonding timelock script. - */ - buildUnbondingTimelockScript(): Buffer; - /** - * Builds the unbonding script in the form: - * buildSingleKeyScript(stakerPk, true) || - * buildMultiKeyScript(covenantPks, covenantThreshold, false) - * || means combining the scripts - * @returns {Buffer} The unbonding script. - */ - buildUnbondingScript(): Buffer; - /** - * Builds the slashing script for staking in the form: - * buildSingleKeyScript(stakerPk, true) || - * buildMultiKeyScript(finalityProviderPKs, 1, true) || - * buildMultiKeyScript(covenantPks, covenantThreshold, false) - * || means combining the scripts - * The slashing script is a combination of single-key and multi-key scripts. - * The single-key script is used for staker key verification. - * The multi-key script is used for finality provider key verification and covenant key verification. - * @returns {Buffer} The slashing script as a Buffer. - */ - buildSlashingScript(): Buffer; - /** - * Builds the staking scripts. - * @returns {StakingScripts} The staking scripts. - */ - buildScripts(): StakingScripts; - /** - * Builds a single key script in the form: - * buildSingleKeyScript creates a single key script - * OP_CHECKSIGVERIFY (if withVerify is true) - * OP_CHECKSIG (if withVerify is false) - * @param pk - The public key buffer. - * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode. - * @returns The compiled script buffer. - */ - buildSingleKeyScript(pk: Buffer, withVerify: boolean): Buffer; - /** - * Builds a multi-key script in the form: - * OP_CHEKCSIG OP_CHECKSIGADD OP_CHECKSIGADD ... OP_CHECKSIGADD OP_NUMEQUAL - * OP_NUMEQUALVERIFY> - * It validates whether provided keys are unique and the threshold is not greater than number of keys - * If there is only one key provided it will return single key sig script - * @param pks - An array of public keys. - * @param threshold - The required number of valid signers. - * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode. - * @returns The compiled multi-key script as a Buffer. - * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided. - */ - buildMultiKeyScript(pks: Buffer[], threshold: number, withVerify: boolean): Buffer; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js deleted file mode 100644 index 7b6e652baa..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/stakingScript.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StakingScriptData = exports.MAGIC_BYTES_LEN = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const keys_1 = require("../constants/keys"); -exports.MAGIC_BYTES_LEN = 4; -// StakingScriptData is a class that holds the data required for the BTC Staking Script -// and exposes methods for converting it into useful formats -class StakingScriptData { - constructor( - // The `stakerKey` is the public key of the staker without the coordinate bytes. - stakerKey, - // A list of public keys without the coordinate bytes corresponding to the finality providers - // the stake will be delegated to. - // Currently, Babylon does not support restaking, so this should contain only a single item. - finalityProviderKeys, - // A list of the public keys without the coordinate bytes corresponding to - // the covenant emulators. - // This is a parameter of the Babylon system and should be retrieved from there. - covenantKeys, - // The number of covenant emulator signatures required for a transaction - // to be valid. - // This is a parameter of the Babylon system and should be retrieved from there. - covenantThreshold, - // The staking period denoted as a number of BTC blocks. - stakingTimelock, - // The unbonding period denoted as a number of BTC blocks. - // This value should be more than equal than the minimum unbonding time of the - // Babylon system. - unbondingTimelock) { - if (!stakerKey || - !finalityProviderKeys || - !covenantKeys || - !covenantThreshold || - !stakingTimelock || - !unbondingTimelock) { - throw new Error("Missing required input values"); - } - this.stakerKey = stakerKey; - this.finalityProviderKeys = finalityProviderKeys; - this.covenantKeys = covenantKeys; - this.covenantThreshold = covenantThreshold; - this.stakingTimeLock = stakingTimelock; - this.unbondingTimeLock = unbondingTimelock; - // Run the validate method to check if the provided script data is valid - if (!this.validate()) { - throw new Error("Invalid script data provided"); - } - } - /** - * Validates the staking script. - * @returns {boolean} Returns true if the staking script is valid, otherwise false. - */ - validate() { - // check that staker key is the correct length - if (this.stakerKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH) { - return false; - } - // check that finalityProvider keys are the correct length - if (this.finalityProviderKeys.some((finalityProviderKey) => finalityProviderKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { - return false; - } - // check that covenant keys are the correct length - if (this.covenantKeys.some((covenantKey) => covenantKey.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { - return false; - } - // Check whether we have any duplicate keys - const allPks = [ - this.stakerKey, - ...this.finalityProviderKeys, - ...this.covenantKeys, - ]; - const allPksSet = new Set(allPks); - if (allPks.length !== allPksSet.size) { - return false; - } - // check that the threshold is above 0 and less than or equal to - // the size of the covenant emulators set - if (this.covenantThreshold <= 0 || - this.covenantThreshold > this.covenantKeys.length) { - return false; - } - // check that maximum value for staking time is not greater than uint16 and above 0 - if (this.stakingTimeLock <= 0 || this.stakingTimeLock > 65535) { - return false; - } - // check that maximum value for unbonding time is not greater than uint16 and above 0 - if (this.unbondingTimeLock <= 0 || this.unbondingTimeLock > 65535) { - return false; - } - return true; - } - // The staking script allows for multiple finality provider public keys - // to support (re)stake to multiple finality providers - // Covenant members are going to have multiple keys - /** - * Builds a timelock script. - * @param timelock - The timelock value to encode in the script. - * @returns {Buffer} containing the compiled timelock script. - */ - buildTimelockScript(timelock) { - return bitcoinjs_lib_1.script.compile([ - this.stakerKey, - bitcoinjs_lib_1.opcodes.OP_CHECKSIGVERIFY, - bitcoinjs_lib_1.script.number.encode(timelock), - bitcoinjs_lib_1.opcodes.OP_CHECKSEQUENCEVERIFY, - ]); - } - /** - * Builds the staking timelock script. - * Only holder of private key for given pubKey can spend after relative lock time - * Creates the timelock script in the form: - * - * OP_CHECKSIGVERIFY - * - * OP_CHECKSEQUENCEVERIFY - * @returns {Buffer} The staking timelock script. - */ - buildStakingTimelockScript() { - return this.buildTimelockScript(this.stakingTimeLock); - } - /** - * Builds the unbonding timelock script. - * Creates the unbonding timelock script in the form: - * - * OP_CHECKSIGVERIFY - * - * OP_CHECKSEQUENCEVERIFY - * @returns {Buffer} The unbonding timelock script. - */ - buildUnbondingTimelockScript() { - return this.buildTimelockScript(this.unbondingTimeLock); - } - /** - * Builds the unbonding script in the form: - * buildSingleKeyScript(stakerPk, true) || - * buildMultiKeyScript(covenantPks, covenantThreshold, false) - * || means combining the scripts - * @returns {Buffer} The unbonding script. - */ - buildUnbondingScript() { - return Buffer.concat([ - this.buildSingleKeyScript(this.stakerKey, true), - this.buildMultiKeyScript(this.covenantKeys, this.covenantThreshold, false), - ]); - } - /** - * Builds the slashing script for staking in the form: - * buildSingleKeyScript(stakerPk, true) || - * buildMultiKeyScript(finalityProviderPKs, 1, true) || - * buildMultiKeyScript(covenantPks, covenantThreshold, false) - * || means combining the scripts - * The slashing script is a combination of single-key and multi-key scripts. - * The single-key script is used for staker key verification. - * The multi-key script is used for finality provider key verification and covenant key verification. - * @returns {Buffer} The slashing script as a Buffer. - */ - buildSlashingScript() { - return Buffer.concat([ - this.buildSingleKeyScript(this.stakerKey, true), - this.buildMultiKeyScript(this.finalityProviderKeys, - // The threshold is always 1 as we only need one - // finalityProvider signature to perform slashing - // (only one finalityProvider performs an offence) - 1, - // OP_VERIFY/OP_CHECKSIGVERIFY is added at the end - true), - this.buildMultiKeyScript(this.covenantKeys, this.covenantThreshold, - // No need to add verify since covenants are at the end of the script - false), - ]); - } - /** - * Builds the staking scripts. - * @returns {StakingScripts} The staking scripts. - */ - buildScripts() { - return { - timelockScript: this.buildStakingTimelockScript(), - unbondingScript: this.buildUnbondingScript(), - slashingScript: this.buildSlashingScript(), - unbondingTimelockScript: this.buildUnbondingTimelockScript(), - }; - } - // buildSingleKeyScript and buildMultiKeyScript allow us to reuse functionality - // for creating Bitcoin scripts for the unbonding script and the slashing script - /** - * Builds a single key script in the form: - * buildSingleKeyScript creates a single key script - * OP_CHECKSIGVERIFY (if withVerify is true) - * OP_CHECKSIG (if withVerify is false) - * @param pk - The public key buffer. - * @param withVerify - A boolean indicating whether to include the OP_CHECKSIGVERIFY opcode. - * @returns The compiled script buffer. - */ - buildSingleKeyScript(pk, withVerify) { - // Check public key length - if (pk.length != keys_1.NO_COORD_PK_BYTE_LENGTH) { - throw new Error("Invalid key length"); - } - return bitcoinjs_lib_1.script.compile([ - pk, - withVerify ? bitcoinjs_lib_1.opcodes.OP_CHECKSIGVERIFY : bitcoinjs_lib_1.opcodes.OP_CHECKSIG, - ]); - } - /** - * Builds a multi-key script in the form: - * OP_CHEKCSIG OP_CHECKSIGADD OP_CHECKSIGADD ... OP_CHECKSIGADD OP_NUMEQUAL - * OP_NUMEQUALVERIFY> - * It validates whether provided keys are unique and the threshold is not greater than number of keys - * If there is only one key provided it will return single key sig script - * @param pks - An array of public keys. - * @param threshold - The required number of valid signers. - * @param withVerify - A boolean indicating whether to include the OP_VERIFY opcode. - * @returns The compiled multi-key script as a Buffer. - * @throws {Error} If no keys are provided, if the required number of valid signers is greater than the number of provided keys, or if duplicate keys are provided. - */ - buildMultiKeyScript(pks, threshold, withVerify) { - // Verify that pks is not empty - if (!pks || pks.length === 0) { - throw new Error("No keys provided"); - } - // Check buffer object have expected lengths like checking pks.length - if (pks.some((pk) => pk.length != keys_1.NO_COORD_PK_BYTE_LENGTH)) { - throw new Error("Invalid key length"); - } - // Verify that threshold <= len(pks) - if (threshold > pks.length) { - throw new Error("Required number of valid signers is greater than number of provided keys"); - } - if (pks.length === 1) { - return this.buildSingleKeyScript(pks[0], withVerify); - } - // keys must be sorted - const sortedPks = [...pks].sort(Buffer.compare); - // verify there are no duplicates - for (let i = 0; i < sortedPks.length - 1; ++i) { - if (sortedPks[i].equals(sortedPks[i + 1])) { - throw new Error("Duplicate keys provided"); - } - } - const scriptElements = [sortedPks[0], bitcoinjs_lib_1.opcodes.OP_CHECKSIG]; - for (let i = 1; i < sortedPks.length; i++) { - scriptElements.push(sortedPks[i]); - scriptElements.push(bitcoinjs_lib_1.opcodes.OP_CHECKSIGADD); - } - scriptElements.push(bitcoinjs_lib_1.script.number.encode(threshold)); - if (withVerify) { - scriptElements.push(bitcoinjs_lib_1.opcodes.OP_NUMEQUALVERIFY); - } - else { - scriptElements.push(bitcoinjs_lib_1.opcodes.OP_NUMEQUAL); - } - return bitcoinjs_lib_1.script.compile(scriptElements); - } -} -exports.StakingScriptData = StakingScriptData; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts deleted file mode 100644 index 68afd0e848..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.d.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Psbt, Transaction, networks } from "bitcoinjs-lib"; -import { UTXO } from "../types/UTXO"; -import { PsbtResult, TransactionResult } from "../types/transaction"; -import { CovenantSignature } from "../types/covenantSignatures"; -/** - * Constructs an unsigned BTC Staking transaction in psbt format. - * - * Outputs: - * - psbt: - * - The first output corresponds to the staking script with the specified amount. - * - The second output corresponds to the change from spending the amount and the transaction fee. - * - If a data embed script is provided, it will be added as the second output, and the fee will be the third output. - * - fee: The total fee amount for the transaction. - * - * Inputs: - * - scripts: - * - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types. - * - dataEmbedScript: Optional data embed script. - * - amount: Amount to stake. - * - changeAddress: Address to send the change to. - * - inputUTXOs: All available UTXOs from the wallet. - * - network: Bitcoin network. - * - feeRate: Fee rate in satoshis per byte. - * - publicKeyNoCoord: Public key if the wallet is in taproot mode. - * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height). - * - * @param {Object} scripts - Scripts used to construct the taproot output. - * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript. - * @param {number} amount - The amount to stake. - * @param {string} changeAddress - The address to send the change to. - * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate in satoshis per byte. - * @param {number} [lockHeight] - The optional block height locktime. - * @returns {TransactionResult} - An object containing the unsigned transaction and fee - * @throws Will throw an error if the amount or fee rate is less than or equal - * to 0, if the change address is invalid, or if the public key is invalid. - */ -export declare function stakingTransaction(scripts: { - timelockScript: Buffer; - unbondingScript: Buffer; - slashingScript: Buffer; - dataEmbedScript?: Buffer; -}, amount: number, changeAddress: string, inputUTXOs: UTXO[], network: networks.Network, feeRate: number, lockHeight?: number): TransactionResult; -/** - * Constructs a withdrawal transaction for manually unbonded delegation. - * - * This transaction spends the unbonded output from the staking transaction. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - unbondingTimelockScript: Script for the unbonding timelock condition. - * - slashingScript: Script for the slashing condition. - * - unbondingTx: The unbonding transaction. - * - withdrawalAddress: The address to send the withdrawn funds to. - * - network: The Bitcoin network. - * - feeRate: The fee rate for the transaction in satoshis per byte. - * - * Returns: - * - psbt: The partially signed transaction (PSBT). - * - * @param {Object} scripts - The scripts used in the transaction. - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -export declare function withdrawEarlyUnbondedTransaction(scripts: { - unbondingTimelockScript: Buffer; - slashingScript: Buffer; -}, unbondingTx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number): PsbtResult; -/** - * Constructs a withdrawal transaction for naturally unbonded delegation. - * - * This transaction spends the unbonded output from the staking transaction when the timelock has expired. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - timelockScript: Script for the timelock condition. - * - slashingScript: Script for the slashing condition. - * - unbondingScript: Script for the unbonding condition. - * - tx: The original staking transaction. - * - withdrawalAddress: The address to send the withdrawn funds to. - * - network: The Bitcoin network. - * - feeRate: The fee rate for the transaction in satoshis per byte. - * - outputIndex: The index of the output to be spent in the original transaction (default is 0). - * - * Returns: - * - psbt: The partially signed transaction (PSBT). - * - * @param {Object} scripts - The scripts used in the transaction. - * @param {Transaction} tx - The original staking transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -export declare function withdrawTimelockUnbondedTransaction(scripts: { - timelockScript: Buffer; - slashingScript: Buffer; - unbondingScript: Buffer; -}, tx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number, outputIndex?: number): PsbtResult; -/** - * Constructs a withdrawal transaction for a slashing transaction. - * - * This transaction spends the output from the slashing transaction. - * - * @param {Object} scripts - The unbondingTimelockScript - * We use the unbonding timelock script as the timelock of the slashing transaction. - * This is due to slashing tx timelock is the same as the unbonding timelock. - * @param {Transaction} slashingTx - The slashing transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @param {number} outputIndex - The index of the output to be spent in the original transaction. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -export declare function withdrawSlashingTransaction(scripts: { - unbondingTimelockScript: Buffer; -}, slashingTx: Transaction, withdrawalAddress: string, network: networks.Network, feeRate: number, outputIndex: number): PsbtResult; -/** - * Constructs a slashing transaction for a staking output without prior unbonding. - * - * This transaction spends the staking output of the staking transaction and distributes the funds - * according to the specified slashing rate. - * - * Outputs: - * - The first output sends `input * slashing_rate` funds to the slashing address. - * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - slashingScript: Script for the slashing condition. - * - timelockScript: Script for the timelock condition. - * - unbondingScript: Script for the unbonding condition. - * - unbondingTimelockScript: Script for the unbonding timelock condition. - * - transaction: The original staking 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. - * @param {Transaction} stakingTransaction - The original staking transaction. - * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. - * @param {number} slashingRate - The rate at which the funds are slashed. - * @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. - * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). - */ -export declare function slashTimelockUnbondedTransaction(scripts: { - slashingScript: Buffer; - timelockScript: Buffer; - unbondingScript: Buffer; - unbondingTimelockScript: Buffer; -}, stakingTransaction: Transaction, slashingPkScriptHex: string, slashingRate: number, minimumFee: number, network: networks.Network, outputIndex?: number): { - psbt: Psbt; -}; -/** - * Constructs a slashing transaction for an early unbonded transaction. - * - * This transaction spends the staking output of the staking transaction and distributes the funds - * according to the specified slashing rate. - * - * Transaction outputs: - * - The first output sends `input * slashing_rate` funds to the slashing address. - * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. - * - * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {string} slashingPkScriptHex - The public key script 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. - * @param {networks.Network} network - The Bitcoin network. - * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). - */ -export declare function slashEarlyUnbondedTransaction(scripts: { - slashingScript: Buffer; - unbondingTimelockScript: Buffer; -}, unbondingTx: Transaction, slashingPkScriptHex: string, slashingRate: number, minimumSlashingFee: number, network: networks.Network): { - psbt: Psbt; -}; -export declare function unbondingTransaction(scripts: { - unbondingTimelockScript: Buffer; - slashingScript: Buffer; -}, stakingTx: Transaction, unbondingFee: number, network: networks.Network, outputIndex?: number): TransactionResult; -export declare const createCovenantWitness: (originalWitness: Buffer[], paramsCovenants: Buffer[], covenantSigs: CovenantSignature[], covenantQuorum: number) => Buffer[]; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js b/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js deleted file mode 100644 index 3056092f04..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/staking/transactions.js +++ /dev/null @@ -1,515 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createCovenantWitness = void 0; -exports.stakingTransaction = stakingTransaction; -exports.withdrawEarlyUnbondedTransaction = withdrawEarlyUnbondedTransaction; -exports.withdrawTimelockUnbondedTransaction = withdrawTimelockUnbondedTransaction; -exports.withdrawSlashingTransaction = withdrawSlashingTransaction; -exports.slashTimelockUnbondedTransaction = slashTimelockUnbondedTransaction; -exports.slashEarlyUnbondedTransaction = slashEarlyUnbondedTransaction; -exports.unbondingTransaction = unbondingTransaction; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const dustSat_1 = require("../constants/dustSat"); -const internalPubkey_1 = require("../constants/internalPubkey"); -const btc_1 = require("../utils/btc"); -const fee_1 = require("../utils/fee"); -const utils_1 = require("../utils/fee/utils"); -const staking_1 = require("../utils/staking"); -const psbt_1 = require("../constants/psbt"); -const transaction_1 = require("../constants/transaction"); -// https://bips.xyz/370 -const BTC_LOCKTIME_HEIGHT_TIME_CUTOFF = 500000000; -const BTC_SLASHING_FRACTION_DIGITS = 4; -/** - * Constructs an unsigned BTC Staking transaction in psbt format. - * - * Outputs: - * - psbt: - * - The first output corresponds to the staking script with the specified amount. - * - The second output corresponds to the change from spending the amount and the transaction fee. - * - If a data embed script is provided, it will be added as the second output, and the fee will be the third output. - * - fee: The total fee amount for the transaction. - * - * Inputs: - * - scripts: - * - timelockScript, unbondingScript, slashingScript: Scripts for different transaction types. - * - dataEmbedScript: Optional data embed script. - * - amount: Amount to stake. - * - changeAddress: Address to send the change to. - * - inputUTXOs: All available UTXOs from the wallet. - * - network: Bitcoin network. - * - feeRate: Fee rate in satoshis per byte. - * - publicKeyNoCoord: Public key if the wallet is in taproot mode. - * - lockHeight: Optional block height locktime to set for the transaction (i.e., not mined until the block height). - * - * @param {Object} scripts - Scripts used to construct the taproot output. - * such as timelockScript, unbondingScript, slashingScript, and dataEmbedScript. - * @param {number} amount - The amount to stake. - * @param {string} changeAddress - The address to send the change to. - * @param {UTXO[]} inputUTXOs - All available UTXOs from the wallet. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate in satoshis per byte. - * @param {number} [lockHeight] - The optional block height locktime. - * @returns {TransactionResult} - An object containing the unsigned transaction and fee - * @throws Will throw an error if the amount or fee rate is less than or equal - * to 0, if the change address is invalid, or if the public key is invalid. - */ -function stakingTransaction(scripts, amount, changeAddress, inputUTXOs, network, feeRate, lockHeight) { - // Check that amount and fee are bigger than 0 - if (amount <= 0 || feeRate <= 0) { - throw new Error("Amount and fee rate must be bigger than 0"); - } - // Check whether the change address is a valid Bitcoin address. - if (!(0, btc_1.isValidBitcoinAddress)(changeAddress, network)) { - throw new Error("Invalid change address"); - } - // Build outputs and estimate the fee - const stakingOutputs = (0, staking_1.buildStakingTransactionOutputs)(scripts, network, amount); - const { selectedUTXOs, fee } = (0, fee_1.getStakingTxInputUTXOsAndFees)(inputUTXOs, amount, feeRate, stakingOutputs); - const tx = new bitcoinjs_lib_1.Transaction(); - tx.version = psbt_1.TRANSACTION_VERSION; - for (let i = 0; i < selectedUTXOs.length; ++i) { - const input = selectedUTXOs[i]; - tx.addInput((0, btc_1.transactionIdToHash)(input.txid), input.vout, psbt_1.NON_RBF_SEQUENCE); - } - stakingOutputs.forEach((o) => { - tx.addOutput(o.scriptPubKey, o.value); - }); - // Add a change output only if there's any amount leftover from the inputs - const inputsSum = (0, utils_1.inputValueSum)(selectedUTXOs); - // Check if the change amount is above the dust limit, and if so, add it as a change output - if (inputsSum - (amount + fee) > dustSat_1.BTC_DUST_SAT) { - tx.addOutput(bitcoinjs_lib_1.address.toOutputScript(changeAddress, network), inputsSum - (amount + fee)); - } - // Set the locktime field if provided. If not provided, the locktime will be set to 0 by default - // Only height based locktime is supported - if (lockHeight) { - if (lockHeight >= BTC_LOCKTIME_HEIGHT_TIME_CUTOFF) { - throw new Error("Invalid lock height"); - } - tx.locktime = lockHeight; - } - return { - transaction: tx, - fee, - }; -} -/** - * Constructs a withdrawal transaction for manually unbonded delegation. - * - * This transaction spends the unbonded output from the staking transaction. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - unbondingTimelockScript: Script for the unbonding timelock condition. - * - slashingScript: Script for the slashing condition. - * - unbondingTx: The unbonding transaction. - * - withdrawalAddress: The address to send the withdrawn funds to. - * - network: The Bitcoin network. - * - feeRate: The fee rate for the transaction in satoshis per byte. - * - * Returns: - * - psbt: The partially signed transaction (PSBT). - * - * @param {Object} scripts - The scripts used in the transaction. - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -function withdrawEarlyUnbondedTransaction(scripts, unbondingTx, withdrawalAddress, network, feeRate) { - const scriptTree = [ - { - output: scripts.slashingScript, - }, - { output: scripts.unbondingTimelockScript }, - ]; - return withdrawalTransaction({ - timelockScript: scripts.unbondingTimelockScript, - }, scriptTree, unbondingTx, withdrawalAddress, network, feeRate, 0); -} -/** - * Constructs a withdrawal transaction for naturally unbonded delegation. - * - * This transaction spends the unbonded output from the staking transaction when the timelock has expired. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - timelockScript: Script for the timelock condition. - * - slashingScript: Script for the slashing condition. - * - unbondingScript: Script for the unbonding condition. - * - tx: The original staking transaction. - * - withdrawalAddress: The address to send the withdrawn funds to. - * - network: The Bitcoin network. - * - feeRate: The fee rate for the transaction in satoshis per byte. - * - outputIndex: The index of the output to be spent in the original transaction (default is 0). - * - * Returns: - * - psbt: The partially signed transaction (PSBT). - * - * @param {Object} scripts - The scripts used in the transaction. - * @param {Transaction} tx - The original staking transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @param {number} [outputIndex=0] - The index of the output to be spent in the original transaction. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -function withdrawTimelockUnbondedTransaction(scripts, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { - const scriptTree = [ - { - output: scripts.slashingScript, - }, - [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], - ]; - return withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex); -} -/** - * Constructs a withdrawal transaction for a slashing transaction. - * - * This transaction spends the output from the slashing transaction. - * - * @param {Object} scripts - The unbondingTimelockScript - * We use the unbonding timelock script as the timelock of the slashing transaction. - * This is due to slashing tx timelock is the same as the unbonding timelock. - * @param {Transaction} slashingTx - The slashing transaction. - * @param {string} withdrawalAddress - The address to send the withdrawn funds to. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} feeRate - The fee rate for the transaction in satoshis per byte. - * @param {number} outputIndex - The index of the output to be spent in the original transaction. - * @returns {PsbtResult} An object containing the partially signed transaction (PSBT). - */ -function withdrawSlashingTransaction(scripts, slashingTx, withdrawalAddress, network, feeRate, outputIndex) { - const scriptTree = { output: scripts.unbondingTimelockScript }; - return withdrawalTransaction({ - timelockScript: scripts.unbondingTimelockScript, - }, scriptTree, slashingTx, withdrawalAddress, network, feeRate, outputIndex); -} -// withdrawalTransaction generates a transaction that -// spends the staking output of the staking transaction -function withdrawalTransaction(scripts, scriptTree, tx, withdrawalAddress, network, feeRate, outputIndex = 0) { - // Check that withdrawal feeRate is bigger than 0 - if (feeRate <= 0) { - throw new Error("Withdrawal feeRate must be bigger than 0"); - } - // Check that outputIndex is bigger or equal to 0 - if (outputIndex < 0) { - throw new Error("Output index must be bigger or equal to 0"); - } - // position of time in the timelock script - const timePosition = 2; - const decompiled = bitcoinjs_lib_1.script.decompile(scripts.timelockScript); - if (!decompiled) { - throw new Error("Timelock script is not valid"); - } - let timelock = 0; - // if the timelock is a buffer, it means it's a number bigger than 16 blocks - if (typeof decompiled[timePosition] !== "number") { - const timeBuffer = decompiled[timePosition]; - timelock = bitcoinjs_lib_1.script.number.decode(timeBuffer); - } - else { - // in case timelock is <= 16 it will be a number, not a buffer - const wrap = decompiled[timePosition] % 16; - timelock = wrap === 0 ? 16 : wrap; - } - const redeem = { - output: scripts.timelockScript, - redeemVersion: transaction_1.REDEEM_VERSION, - }; - const p2tr = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree, - redeem, - network, - }); - const tapLeafScript = { - leafVersion: redeem.redeemVersion, - script: redeem.output, - controlBlock: p2tr.witness[p2tr.witness.length - 1], - }; - const psbt = new bitcoinjs_lib_1.Psbt({ network }); - // only transactions with version 2 can trigger OP_CHECKSEQUENCEVERIFY - // https://github.com/btcsuite/btcd/blob/master/txscript/opcode.go#L1174 - psbt.setVersion(psbt_1.TRANSACTION_VERSION); - psbt.addInput({ - hash: tx.getHash(), - index: outputIndex, - tapInternalKey: internalPubkey_1.internalPubkey, - witnessUtxo: { - value: tx.outs[outputIndex].value, - script: tx.outs[outputIndex].script, - }, - tapLeafScript: [tapLeafScript], - sequence: timelock, - }); - const estimatedFee = (0, fee_1.getWithdrawTxFee)(feeRate); - const outputValue = tx.outs[outputIndex].value - estimatedFee; - if (outputValue < 0) { - throw new Error("Not enough funds to cover the fee for withdrawal transaction"); - } - if (outputValue < dustSat_1.BTC_DUST_SAT) { - throw new Error("Output value is less than dust limit"); - } - psbt.addOutput({ - address: withdrawalAddress, - value: outputValue, - }); - // Withdraw transaction has no time-based restrictions and can be included - // in the next block immediately. - psbt.setLocktime(0); - return { - psbt, - fee: estimatedFee, - }; -} -/** - * Constructs a slashing transaction for a staking output without prior unbonding. - * - * This transaction spends the staking output of the staking transaction and distributes the funds - * according to the specified slashing rate. - * - * Outputs: - * - The first output sends `input * slashing_rate` funds to the slashing address. - * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. - * - * Inputs: - * - scripts: Scripts used to construct the taproot output. - * - slashingScript: Script for the slashing condition. - * - timelockScript: Script for the timelock condition. - * - unbondingScript: Script for the unbonding condition. - * - unbondingTimelockScript: Script for the unbonding timelock condition. - * - transaction: The original staking 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. - * @param {Transaction} stakingTransaction - The original staking transaction. - * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. - * @param {number} slashingRate - The rate at which the funds are slashed. - * @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. - * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). - */ -function slashTimelockUnbondedTransaction(scripts, stakingTransaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { - const slashingScriptTree = [ - { - output: scripts.slashingScript, - }, - [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], - ]; - return slashingTransaction({ - unbondingTimelockScript: scripts.unbondingTimelockScript, - slashingScript: scripts.slashingScript, - }, slashingScriptTree, stakingTransaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex); -} -/** - * Constructs a slashing transaction for an early unbonded transaction. - * - * This transaction spends the staking output of the staking transaction and distributes the funds - * according to the specified slashing rate. - * - * Transaction outputs: - * - The first output sends `input * slashing_rate` funds to the slashing address. - * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. - * - * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript - * @param {Transaction} unbondingTx - The unbonding transaction. - * @param {string} slashingPkScriptHex - The public key script 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. - * @param {networks.Network} network - The Bitcoin network. - * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). - */ -function slashEarlyUnbondedTransaction(scripts, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network) { - const unbondingScriptTree = [ - { - output: scripts.slashingScript, - }, - { - output: scripts.unbondingTimelockScript, - }, - ]; - return slashingTransaction({ - unbondingTimelockScript: scripts.unbondingTimelockScript, - slashingScript: scripts.slashingScript, - }, unbondingScriptTree, unbondingTx, slashingPkScriptHex, slashingRate, minimumSlashingFee, network, 0); -} -/** - * Constructs a slashing transaction for an on-demand unbonding. - * - * This transaction spends the staking output of the staking transaction and distributes the funds - * according to the specified slashing rate. - * - * Transaction outputs: - * - The first output sends `input * slashing_rate` funds to the slashing address. - * - The second output sends `input * (1 - slashing_rate) - fee` funds back to the user's address. - * - * @param {Object} scripts - The scripts used in the transaction. e.g slashingScript, unbondingTimelockScript - * @param {Transaction} transaction - The original staking/unbonding transaction. - * @param {string} slashingPkScriptHex - The public key script to send the slashed funds to. - * @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. - * @returns {{ psbt: Psbt }} An object containing the partially signed transaction (PSBT). - */ -function slashingTransaction(scripts, scriptTree, transaction, slashingPkScriptHex, slashingRate, minimumFee, network, outputIndex = 0) { - // Check that slashing rate and minimum fee are 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(BTC_SLASHING_FRACTION_DIGITS)); - // 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 || !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 = { - output: scripts.slashingScript, - redeemVersion: transaction_1.REDEEM_VERSION, - }; - const p2tr = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree, - redeem, - network, - }); - const tapLeafScript = { - leafVersion: redeem.redeemVersion, - script: redeem.output, - 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.round(stakingAmount * slashingRate); - // Compute the slashing output - const slashingOutput = Buffer.from(slashingPkScriptHex, "hex"); - // If OP_RETURN is not included, the slashing amount must be greater than the - // dust limit. - if (bitcoinjs_lib_1.opcodes.OP_RETURN != slashingOutput[0]) { - if (slashingAmount <= dustSat_1.BTC_DUST_SAT) { - throw new Error("Slashing amount is less than dust limit"); - } - } - const userFunds = stakingAmount - slashingAmount - minimumFee; - if (userFunds <= dustSat_1.BTC_DUST_SAT) { - throw new Error("User funds are less than dust limit"); - } - const psbt = new bitcoinjs_lib_1.Psbt({ network }); - psbt.setVersion(psbt_1.TRANSACTION_VERSION); - psbt.addInput({ - hash: transaction.getHash(), - index: outputIndex, - tapInternalKey: internalPubkey_1.internalPubkey, - witnessUtxo: { - value: stakingAmount, - script: transaction.outs[outputIndex].script, - }, - tapLeafScript: [tapLeafScript], - // not RBF-able - sequence: psbt_1.NON_RBF_SEQUENCE, - }); - // Add the slashing output - psbt.addOutput({ - script: slashingOutput, - value: slashingAmount, - }); - // Change output contains unbonding timelock script - const changeOutput = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree: { output: scripts.unbondingTimelockScript }, - network, - }); - // Add the change output - psbt.addOutput({ - address: changeOutput.address, - value: userFunds, - }); - // Slashing transaction has no time-based restrictions and can be included - // in the next block immediately. - psbt.setLocktime(0); - return { psbt }; -} -function unbondingTransaction(scripts, stakingTx, unbondingFee, network, outputIndex = 0) { - // Check that transaction fee is bigger than 0 - if (unbondingFee <= 0) { - throw new Error("Unbonding fee must be bigger than 0"); - } - // Check that outputIndex is bigger or equal to 0 - if (outputIndex < 0) { - throw new Error("Output index must be bigger or equal to 0"); - } - const tx = new bitcoinjs_lib_1.Transaction(); - tx.version = psbt_1.TRANSACTION_VERSION; - tx.addInput(stakingTx.getHash(), outputIndex, psbt_1.NON_RBF_SEQUENCE); - const unbondingOutputInfo = (0, staking_1.deriveUnbondingOutputInfo)(scripts, network); - const outputValue = stakingTx.outs[outputIndex].value - unbondingFee; - if (outputValue < dustSat_1.BTC_DUST_SAT) { - throw new Error("Output value is less than dust limit for unbonding transaction"); - } - // Add the unbonding output - if (!unbondingOutputInfo.outputAddress) { - throw new Error("Unbonding output address is not defined"); - } - tx.addOutput(unbondingOutputInfo.scriptPubKey, outputValue); - // Unbonding transaction has no time-based restrictions and can be included - // in the next block immediately. - tx.locktime = 0; - return { - transaction: tx, - fee: unbondingFee, - }; -} -// This function attaches covenant signatures as the transaction's witness -// Note that the witness script expects exactly covenantQuorum number of signatures -// to match the covenant parameters. -const createCovenantWitness = (originalWitness, paramsCovenants, covenantSigs, covenantQuorum) => { - if (covenantSigs.length < covenantQuorum) { - throw new Error(`Not enough covenant signatures. Required: ${covenantQuorum}, ` - + `got: ${covenantSigs.length}`); - } - // Verify all btcPkHex from covenantSigs exist in paramsCovenants - for (const sig of covenantSigs) { - const btcPkHexBuf = Buffer.from(sig.btcPkHex, "hex"); - if (!paramsCovenants.some(covenant => covenant.equals(btcPkHexBuf))) { - throw new Error(`Covenant signature public key ${sig.btcPkHex} not found in params covenants`); - } - } - // We only take exactly covenantQuorum number of signatures, even if more are provided. - // Including extra signatures will cause the unbonding transaction to fail validation. - // This is because the witness script expects exactly covenantQuorum number of signatures - // to match the covenant parameters. - const covenantSigsBuffers = covenantSigs - .slice(0, covenantQuorum) - .map((sig) => ({ - btcPkHex: Buffer.from(sig.btcPkHex, "hex"), - sigHex: Buffer.from(sig.sigHex, "hex"), - })); - // we need covenant from params to be sorted in reverse order - const paramsCovenantsSorted = [...paramsCovenants] - .sort(Buffer.compare) - .reverse(); - const composedCovenantSigs = paramsCovenantsSorted.map((covenant) => { - // in case there's covenant with this btc_pk_hex we return the sig - // otherwise we return empty Buffer - const covenantSig = covenantSigsBuffers.find((sig) => sig.btcPkHex.compare(covenant) === 0); - return (covenantSig === null || covenantSig === void 0 ? void 0 : covenantSig.sigHex) || Buffer.alloc(0); - }); - return [...composedCovenantSigs, ...originalWitness]; -}; -exports.createCovenantWitness = createCovenantWitness; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts deleted file mode 100644 index 5e475b085a..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface UTXO { - txid: string; - vout: number; - value: number; - scriptPubKey: string; - rawTxHex?: string; - redeemScript?: string; - witnessScript?: string; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js deleted file mode 100644 index c8ad2e549b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/UTXO.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts deleted file mode 100644 index af46404b5b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface CovenantSignature { - btcPkHex: string; - sigHex: string; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js deleted file mode 100644 index c8ad2e549b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/covenantSignatures.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts deleted file mode 100644 index 92de588970..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./params"; -export * from "./transaction"; -export * from "./UTXO"; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js deleted file mode 100644 index 0683e30272..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/index.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./params"), exports); -__exportStar(require("./transaction"), exports); -__exportStar(require("./UTXO"), exports); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts deleted file mode 100644 index fad1535517..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Base interface for staking parameters that define the rules and constraints - * for staking operations. - */ -export interface StakingParams { - covenantNoCoordPks: string[]; - covenantQuorum: number; - unbondingTime: number; - unbondingFeeSat: number; - maxStakingAmountSat: number; - minStakingAmountSat: number; - maxStakingTimeBlocks: number; - minStakingTimeBlocks: number; - slashing?: { - slashingPkScriptHex: string; - slashingRate: number; - minSlashingTxFeeSat: number; - }; -} -/** - * Extension of StakingParams that includes activation height and version information. - * These parameters are used to identify and select the appropriate staking rules at - * different blockchain heights, but do not affect the actual staking transaction content. - */ -export interface VersionedStakingParams extends StakingParams { - btcActivationHeight: number; - version: number; -} -/** - * Extension of VersionedStakingParams that includes a tag field for observability. - */ -export interface ObservableVersionedStakingParams extends VersionedStakingParams { - tag: string; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js deleted file mode 100644 index c8ad2e549b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/params.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts deleted file mode 100644 index c0192b57ed..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PsbtOutput } from "bip174/src/lib/interfaces"; -export type PsbtOutputExtended = PsbtOutputExtendedAddress | PsbtOutputExtendedScript; -interface PsbtOutputExtendedAddress extends PsbtOutput { - address: string; - value: number; -} -interface PsbtOutputExtendedScript extends PsbtOutput { - script: Buffer; - value: number; -} -export declare const isPsbtOutputExtendedAddress: (output: PsbtOutputExtended) => output is PsbtOutputExtendedAddress; -export type TransactionOutput = { - scriptPubKey: Buffer; - value: number; -}; -export {}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js deleted file mode 100644 index d09308781e..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/psbtOutputs.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isPsbtOutputExtendedAddress = void 0; -const isPsbtOutputExtendedAddress = (output) => { - return output.address !== undefined; -}; -exports.isPsbtOutputExtendedAddress = isPsbtOutputExtendedAddress; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts deleted file mode 100644 index 5298388ae6..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Psbt, Transaction } from "bitcoinjs-lib"; -/** - * PsbtResult is an object containing a partially signed transaction and its fee - */ -export interface PsbtResult { - psbt: Psbt; - fee: number; -} -/** - * TransactionResult is an object containing an unsigned transaction and its fee - */ -export interface TransactionResult { - transaction: Transaction; - fee: number; -} diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js b/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js deleted file mode 100644 index c8ad2e549b..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/types/transaction.js +++ /dev/null @@ -1,2 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts deleted file mode 100644 index 047058574f..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Validates a Babylon address. Babylon addresses are encoded in Bech32 format - * and have a prefix of "bbn". - * @param address - The address to validate. - * @returns True if the address is valid, false otherwise. - */ -export declare const isValidBabylonAddress: (address: string) => boolean; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js deleted file mode 100644 index 0d301d156d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/babylon.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isValidBabylonAddress = void 0; -const encoding_1 = require("@cosmjs/encoding"); -/** - * Validates a Babylon address. Babylon addresses are encoded in Bech32 format - * and have a prefix of "bbn". - * @param address - The address to validate. - * @returns True if the address is valid, false otherwise. - */ -const isValidBabylonAddress = (address) => { - try { - const { prefix } = (0, encoding_1.fromBech32)(address); - return prefix === "bbn"; - } - catch (error) { - return false; - } -}; -exports.isValidBabylonAddress = isValidBabylonAddress; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts deleted file mode 100644 index c24f33a3a3..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { networks } from "bitcoinjs-lib"; -export declare const initBTCCurve: () => void; -/** - * Check whether the given address is a valid Bitcoin address. - * - * @param {string} btcAddress - The Bitcoin address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is valid, otherwise false. - */ -export declare const isValidBitcoinAddress: (btcAddress: string, network: networks.Network) => boolean; -/** - * Check whether the given address is a Taproot address. - * - * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is a Taproot address, otherwise false. - */ -export declare const isTaproot: (taprootAddress: string, network: networks.Network) => boolean; -/** - * Check whether the given address is a Native SegWit address. - * - * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is a Native SegWit address, otherwise false. - */ -export declare const isNativeSegwit: (segwitAddress: string, network: networks.Network) => boolean; -/** - * Check whether the given public key is a valid public key without a coordinate. - * - * @param {string} pkWithNoCoord - public key without the coordinate. - * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false. - */ -export declare const isValidNoCoordPublicKey: (pkWithNoCoord: string) => boolean; -/** - * Get the public key without the coordinate. - * - * @param {string} pkHex - The public key in hex, with or without the coordinate. - * @returns {string} - The public key without the coordinate in hex. - * @throws {Error} - If the public key is invalid. - */ -export declare const getPublicKeyNoCoord: (pkHex: string) => String; -/** - * Convert a transaction id to a hash. in buffer format. - * - * @param {string} txId - The transaction id. - * @returns {Buffer} - The transaction hash. - */ -export declare const transactionIdToHash: (txId: string) => Buffer; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js deleted file mode 100644 index 4402ee8be1..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/btc.js +++ /dev/null @@ -1,179 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.transactionIdToHash = exports.getPublicKeyNoCoord = exports.isValidNoCoordPublicKey = exports.isNativeSegwit = exports.isTaproot = exports.isValidBitcoinAddress = exports.initBTCCurve = void 0; -const ecc = __importStar(require("@bitcoin-js/tiny-secp256k1-asmjs")); -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const keys_1 = require("../constants/keys"); -// Initialize elliptic curve library -const initBTCCurve = () => { - (0, bitcoinjs_lib_1.initEccLib)(ecc); -}; -exports.initBTCCurve = initBTCCurve; -/** - * Check whether the given address is a valid Bitcoin address. - * - * @param {string} btcAddress - The Bitcoin address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is valid, otherwise false. - */ -const isValidBitcoinAddress = (btcAddress, network) => { - try { - return !!bitcoinjs_lib_1.address.toOutputScript(btcAddress, network); - } - catch (error) { - return false; - } -}; -exports.isValidBitcoinAddress = isValidBitcoinAddress; -/** - * Check whether the given address is a Taproot address. - * - * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is a Taproot address, otherwise false. - */ -const isTaproot = (taprootAddress, network) => { - try { - const decoded = bitcoinjs_lib_1.address.fromBech32(taprootAddress); - if (decoded.version !== 1) { - return false; - } - // Compare network properties instead of object reference - // The bech32 is hardcoded in the bitcoinjs-lib library. - // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36 - if (network.bech32 === bitcoinjs_lib_1.networks.bitcoin.bech32) { - // Check if address starts with "bc1p" - return taprootAddress.startsWith("bc1p"); - } - else if (network.bech32 === bitcoinjs_lib_1.networks.testnet.bech32) { - // signet, regtest and testnet taproot addresses start with "tb1p" or "sb1p" - return taprootAddress.startsWith("tb1p") || taprootAddress.startsWith("sb1p"); - } - return false; - } - catch (error) { - return false; - } -}; -exports.isTaproot = isTaproot; -/** - * Check whether the given address is a Native SegWit address. - * - * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check. - * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin). - * @returns {boolean} - True if the address is a Native SegWit address, otherwise false. - */ -const isNativeSegwit = (segwitAddress, network) => { - try { - const decoded = bitcoinjs_lib_1.address.fromBech32(segwitAddress); - if (decoded.version !== 0) { - return false; - } - // Compare network properties instead of object reference - // The bech32 is hardcoded in the bitcoinjs-lib library. - // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36 - if (network.bech32 === bitcoinjs_lib_1.networks.bitcoin.bech32) { - // Check if address starts with "bc1q" - return segwitAddress.startsWith("bc1q"); - } - else if (network.bech32 === bitcoinjs_lib_1.networks.testnet.bech32) { - // testnet native segwit addresses start with "tb1q" - return segwitAddress.startsWith("tb1q"); - } - return false; - } - catch (error) { - return false; - } -}; -exports.isNativeSegwit = isNativeSegwit; -/** - * Check whether the given public key is a valid public key without a coordinate. - * - * @param {string} pkWithNoCoord - public key without the coordinate. - * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false. - */ -const isValidNoCoordPublicKey = (pkWithNoCoord) => { - try { - const keyBuffer = Buffer.from(pkWithNoCoord, 'hex'); - return validateNoCoordPublicKeyBuffer(keyBuffer); - } - catch (error) { - return false; - } -}; -exports.isValidNoCoordPublicKey = isValidNoCoordPublicKey; -/** - * Get the public key without the coordinate. - * - * @param {string} pkHex - The public key in hex, with or without the coordinate. - * @returns {string} - The public key without the coordinate in hex. - * @throws {Error} - If the public key is invalid. - */ -const getPublicKeyNoCoord = (pkHex) => { - const publicKey = Buffer.from(pkHex, "hex"); - const publicKeyNoCoordBuffer = publicKey.length === keys_1.NO_COORD_PK_BYTE_LENGTH - ? publicKey - : publicKey.subarray(1, 33); - // Validate the public key without coordinate - if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) { - throw new Error("Invalid public key without coordinate"); - } - return publicKeyNoCoordBuffer.toString("hex"); -}; -exports.getPublicKeyNoCoord = getPublicKeyNoCoord; -const validateNoCoordPublicKeyBuffer = (pkBuffer) => { - if (pkBuffer.length !== keys_1.NO_COORD_PK_BYTE_LENGTH) { - return false; - } - // Try both compressed forms: y-coordinate even (0x02) and y-coordinate odd (0x03) - const compressedKeyEven = Buffer.concat([Buffer.from([0x02]), pkBuffer]); - const compressedKeyOdd = Buffer.concat([Buffer.from([0x03]), pkBuffer]); - return (ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd)); -}; -/** - * Convert a transaction id to a hash. in buffer format. - * - * @param {string} txId - The transaction id. - * @returns {Buffer} - The transaction hash. - */ -const transactionIdToHash = (txId) => { - if (txId === "") { - throw new Error("Transaction id cannot be empty"); - } - return Buffer.from(txId, 'hex').reverse(); -}; -exports.transactionIdToHash = transactionIdToHash; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts deleted file mode 100644 index 8d48f76130..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { UTXO } from "../../types/UTXO"; -import { TransactionOutput } from "../../types/psbtOutputs"; -/** - * Selects UTXOs and calculates the fee for a staking transaction. - * This method selects the highest value UTXOs from all available UTXOs to - * cover the staking amount and the transaction fees. - * The formula used is: - * - * totalFee = (inputSize + outputSize) * feeRate + buffer - * where outputSize may or may not include the change output size depending on the remaining value. - * - * @param availableUTXOs - All available UTXOs from the wallet. - * @param stakingAmount - The amount to stake. - * @param feeRate - The fee rate in satoshis per byte. - * @param outputs - The outputs in the transaction. - * @returns An object containing the selected UTXOs and the fee. - * @throws Will throw an error if there are insufficient funds or if the fee cannot be calculated. - */ -export declare const getStakingTxInputUTXOsAndFees: (availableUTXOs: UTXO[], stakingAmount: number, feeRate: number, outputs: TransactionOutput[]) => { - selectedUTXOs: UTXO[]; - fee: number; -}; -/** - * Calculates the estimated fee for a withdrawal transaction. - * The fee calculation is based on estimated constants for input size, - * output size, and additional overhead specific to withdrawal transactions. - * Due to the slightly larger size of withdrawal transactions, an additional - * buffer is included to account for this difference. - * - * @param feeRate - The fee rate in satoshis per vbyte. - * @returns The estimated fee for a withdrawal transaction in satoshis. - */ -export declare const getWithdrawTxFee: (feeRate: number) => number; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js deleted file mode 100644 index a874ab5527..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/index.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getWithdrawTxFee = exports.getStakingTxInputUTXOsAndFees = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const dustSat_1 = require("../../constants/dustSat"); -const fee_1 = require("../../constants/fee"); -const utils_1 = require("./utils"); -/** - * Selects UTXOs and calculates the fee for a staking transaction. - * This method selects the highest value UTXOs from all available UTXOs to - * cover the staking amount and the transaction fees. - * The formula used is: - * - * totalFee = (inputSize + outputSize) * feeRate + buffer - * where outputSize may or may not include the change output size depending on the remaining value. - * - * @param availableUTXOs - All available UTXOs from the wallet. - * @param stakingAmount - The amount to stake. - * @param feeRate - The fee rate in satoshis per byte. - * @param outputs - The outputs in the transaction. - * @returns An object containing the selected UTXOs and the fee. - * @throws Will throw an error if there are insufficient funds or if the fee cannot be calculated. - */ -const getStakingTxInputUTXOsAndFees = (availableUTXOs, stakingAmount, feeRate, outputs) => { - if (availableUTXOs.length === 0) { - throw new Error("Insufficient funds"); - } - const validUTXOs = availableUTXOs.filter((utxo) => { - const script = Buffer.from(utxo.scriptPubKey, "hex"); - return !!bitcoinjs_lib_1.script.decompile(script); - }); - if (validUTXOs.length === 0) { - throw new Error("Insufficient funds: no valid UTXOs available for staking"); - } - // Sort available UTXOs from highest to lowest value - const sortedUTXOs = validUTXOs.sort((a, b) => b.value - a.value); - const selectedUTXOs = []; - let accumulatedValue = 0; - let estimatedFee = 0; - for (const utxo of sortedUTXOs) { - selectedUTXOs.push(utxo); - accumulatedValue += utxo.value; - // Calculate the fee for the current set of UTXOs and outputs - const estimatedSize = getEstimatedSize(selectedUTXOs, outputs); - estimatedFee = estimatedSize * feeRate + rateBasedTxBufferFee(feeRate); - // Check if there will be any change left after the staking amount and fee. - // If there is, a change output needs to be added, which also comes with an additional fee. - if (accumulatedValue - (stakingAmount + estimatedFee) > dustSat_1.BTC_DUST_SAT) { - estimatedFee += (0, utils_1.getEstimatedChangeOutputSize)() * feeRate; - } - if (accumulatedValue >= stakingAmount + estimatedFee) { - break; - } - } - if (accumulatedValue < stakingAmount + estimatedFee) { - throw new Error("Insufficient funds: unable to gather enough UTXOs to cover the staking amount and fees"); - } - return { - selectedUTXOs, - fee: estimatedFee, - }; -}; -exports.getStakingTxInputUTXOsAndFees = getStakingTxInputUTXOsAndFees; -/** - * Calculates the estimated fee for a withdrawal transaction. - * The fee calculation is based on estimated constants for input size, - * output size, and additional overhead specific to withdrawal transactions. - * Due to the slightly larger size of withdrawal transactions, an additional - * buffer is included to account for this difference. - * - * @param feeRate - The fee rate in satoshis per vbyte. - * @returns The estimated fee for a withdrawal transaction in satoshis. - */ -const getWithdrawTxFee = (feeRate) => { - const inputSize = fee_1.P2TR_INPUT_SIZE; - const outputSize = (0, utils_1.getEstimatedChangeOutputSize)(); - return (feeRate * - (inputSize + - outputSize + - fee_1.TX_BUFFER_SIZE_OVERHEAD + - fee_1.WITHDRAW_TX_BUFFER_SIZE) + - rateBasedTxBufferFee(feeRate)); -}; -exports.getWithdrawTxFee = getWithdrawTxFee; -/** - * Calculates the estimated transaction size using a heuristic formula which - * includes the input size, output size, and a fixexd buffer for the transaction size. - * The formula used is: - * - * totalSize = inputSize + outputSize + TX_BUFFER_SIZE_OVERHEAD - * - * @param inputUtxos - The UTXOs used as inputs in the transaction. - * @param outputs - The outputs in the transaction. - * @returns The estimated transaction size in bytes. - */ -const getEstimatedSize = (inputUtxos, outputs) => { - // Estimate the input size - const inputSize = inputUtxos.reduce((acc, u) => { - const script = Buffer.from(u.scriptPubKey, "hex"); - const decompiledScript = bitcoinjs_lib_1.script.decompile(script); - if (!decompiledScript) { - // Skip UTXOs with scripts that cannot be decompiled - return acc; - } - return acc + (0, utils_1.getInputSizeByScript)(script); - }, 0); - // Estimate the output size - const outputSize = outputs.reduce((acc, output) => { - if ((0, utils_1.isOP_RETURN)(output.scriptPubKey)) { - return (acc + - output.scriptPubKey.length + - fee_1.OP_RETURN_OUTPUT_VALUE_SIZE + - fee_1.OP_RETURN_VALUE_SERIALIZE_SIZE); - } - return acc + fee_1.MAX_NON_LEGACY_OUTPUT_SIZE; - }, 0); - return inputSize + outputSize + fee_1.TX_BUFFER_SIZE_OVERHEAD; -}; -/** - * Adds a buffer to the transaction size-based fee calculation if the fee rate is low. - * Some wallets have a relayer fee requirement, which means if the fee rate is - * less than or equal to WALLET_RELAY_FEE_RATE_THRESHOLD (2 satoshis per byte), - * there is a risk that the fee might not be sufficient to get the transaction relayed. - * To mitigate this risk, we add a buffer to the fee calculation to ensure that - * the transaction can be relayed. - * - * If the fee rate is less than or equal to WALLET_RELAY_FEE_RATE_THRESHOLD, a fixed buffer is added - * (LOW_RATE_ESTIMATION_ACCURACY_BUFFER). If the fee rate is higher, no buffer is added. - * - * @param feeRate - The fee rate in satoshis per byte. - * @returns The buffer amount in satoshis to be added to the transaction fee. - */ -const rateBasedTxBufferFee = (feeRate) => { - return feeRate <= fee_1.WALLET_RELAY_FEE_RATE_THRESHOLD - ? fee_1.LOW_RATE_ESTIMATION_ACCURACY_BUFFER - : 0; -}; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts deleted file mode 100644 index 116901cc82..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UTXO } from "../../types/UTXO"; -export declare const isOP_RETURN: (script: Buffer) => boolean; -/** - * Determines the size of a transaction input based on its script type. - * - * @param script - The script of the input. - * @returns The estimated size of the input in bytes. - */ -export declare const getInputSizeByScript: (script: Buffer) => number; -/** - * Returns the estimated size for a change output. - * This is used when the transaction has a change output to a particular address. - * - * @returns The estimated size for a change output in bytes. - */ -export declare const getEstimatedChangeOutputSize: () => number; -/** - * Returns the sum of the values of the UTXOs. - * - * @param inputUTXOs - The UTXOs to sum the values of. - * @returns The sum of the values of the UTXOs in satoshis. - */ -export declare const inputValueSum: (inputUTXOs: UTXO[]) => number; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js deleted file mode 100644 index 087a4b916d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/fee/utils.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.inputValueSum = exports.getEstimatedChangeOutputSize = exports.getInputSizeByScript = exports.isOP_RETURN = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const fee_1 = require("../../constants/fee"); -// Helper function to check if a script is OP_RETURN -const isOP_RETURN = (script) => { - const decompiled = bitcoinjs_lib_1.script.decompile(script); - return !!decompiled && decompiled[0] === bitcoinjs_lib_1.opcodes.OP_RETURN; -}; -exports.isOP_RETURN = isOP_RETURN; -/** - * Determines the size of a transaction input based on its script type. - * - * @param script - The script of the input. - * @returns The estimated size of the input in bytes. - */ -const getInputSizeByScript = (script) => { - // Check if input is in the format of "00 <20-byte public key hash>" - // If yes, it is a P2WPKH input - try { - const { address: p2wpkhAddress } = bitcoinjs_lib_1.payments.p2wpkh({ - output: script, - }); - if (p2wpkhAddress) { - return fee_1.P2WPKH_INPUT_SIZE; - } - // eslint-disable-next-line no-empty - } - catch (error) { } // Ignore errors - // Check if input is in the format of "51 <32-byte public key>" - // If yes, it is a P2TR input - try { - const { address: p2trAddress } = bitcoinjs_lib_1.payments.p2tr({ - output: script, - }); - if (p2trAddress) { - return fee_1.P2TR_INPUT_SIZE; - } - // eslint-disable-next-line no-empty - } - catch (error) { } // Ignore errors - // Otherwise, assume the input is largest P2PKH address type - return fee_1.DEFAULT_INPUT_SIZE; -}; -exports.getInputSizeByScript = getInputSizeByScript; -/** - * Returns the estimated size for a change output. - * This is used when the transaction has a change output to a particular address. - * - * @returns The estimated size for a change output in bytes. - */ -const getEstimatedChangeOutputSize = () => { - return fee_1.MAX_NON_LEGACY_OUTPUT_SIZE; -}; -exports.getEstimatedChangeOutputSize = getEstimatedChangeOutputSize; -/** - * Returns the sum of the values of the UTXOs. - * - * @param inputUTXOs - The UTXOs to sum the values of. - * @returns The sum of the values of the UTXOs in satoshis. - */ -const inputValueSum = (inputUTXOs) => { - return inputUTXOs.reduce((acc, utxo) => acc + utxo.value, 0); -}; -exports.inputValueSum = inputValueSum; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts deleted file mode 100644 index aeea8f0017..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Reverses the order of bytes in a buffer. - * @param buffer - The buffer to reverse. - * @returns A new buffer with the bytes reversed. - */ -export declare const reverseBuffer: (buffer: Uint8Array) => Uint8Array; -/** - * Converts a Uint8Array to a hexadecimal string. - * @param uint8Array - The Uint8Array to convert. - * @returns The hexadecimal string. - */ -export declare const uint8ArrayToHex: (uint8Array: Uint8Array) => string; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js deleted file mode 100644 index 883142beeb..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/index.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.uint8ArrayToHex = exports.reverseBuffer = void 0; -/** - * Reverses the order of bytes in a buffer. - * @param buffer - The buffer to reverse. - * @returns A new buffer with the bytes reversed. - */ -const reverseBuffer = (buffer) => { - const clonedBuffer = new Uint8Array(buffer); - if (clonedBuffer.length < 1) - return clonedBuffer; - for (let i = 0, j = clonedBuffer.length - 1; i < clonedBuffer.length / 2; i++, j--) { - let tmp = clonedBuffer[i]; - clonedBuffer[i] = clonedBuffer[j]; - clonedBuffer[j] = tmp; - } - return clonedBuffer; -}; -exports.reverseBuffer = reverseBuffer; -/** - * Converts a Uint8Array to a hexadecimal string. - * @param uint8Array - The Uint8Array to convert. - * @returns The hexadecimal string. - */ -const uint8ArrayToHex = (uint8Array) => { - return Array.from(uint8Array) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join(""); -}; -exports.uint8ArrayToHex = uint8ArrayToHex; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts deleted file mode 100644 index 375e67b627..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.d.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { networks, Transaction } from "bitcoinjs-lib"; -import { TransactionOutput } from "../../types/psbtOutputs"; -import { UTXO } from "../../types/UTXO"; -import { StakingParams } from "../../types/params"; -export interface OutputInfo { - scriptPubKey: Buffer; - outputAddress: string; -} -/** - * Build the staking output for the transaction which contains p2tr output - * with staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} amount - The amount to stake. - * @returns {TransactionOutput[]} - The staking transaction outputs. - * @throws {Error} - If the staking output cannot be built. - */ -export declare const buildStakingTransactionOutputs: (scripts: { - timelockScript: Buffer; - unbondingScript: Buffer; - slashingScript: Buffer; - dataEmbedScript?: Buffer; -}, network: networks.Network, amount: number) => TransactionOutput[]; -/** - * Derive the staking output address from the staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @returns {StakingOutput} - The staking output address and scriptPubKey. - * @throws {StakingError} - If the staking output address cannot be derived. - */ -export declare const deriveStakingOutputInfo: (scripts: { - timelockScript: Buffer; - unbondingScript: Buffer; - slashingScript: Buffer; -}, network: networks.Network) => { - outputAddress: string; - scriptPubKey: Buffer; -}; -/** - * Derive the unbonding output address and scriptPubKey from the staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @returns {OutputInfo} - The unbonding output address and scriptPubKey. - * @throws {StakingError} - If the unbonding output address cannot be derived. - */ -export declare const deriveUnbondingOutputInfo: (scripts: { - unbondingTimelockScript: Buffer; - slashingScript: Buffer; -}, network: networks.Network) => { - outputAddress: string; - scriptPubKey: Buffer; -}; -/** - * Derive the slashing output address and scriptPubKey from the staking scripts. - * - * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the - * unbonding timelock script as the timelock of the slashing transaction. - * This is due to slashing tx timelock is the same as the unbonding timelock. - * @param {networks.Network} network - The Bitcoin network. - * @returns {OutputInfo} - The slashing output address and scriptPubKey. - * @throws {StakingError} - If the slashing output address cannot be derived. - */ -export declare const deriveSlashingOutput: (scripts: { - unbondingTimelockScript: Buffer; -}, network: networks.Network) => { - outputAddress: string; - scriptPubKey: Buffer; -}; -/** - * Find the matching output index for the given transaction. - * - * @param {Transaction} tx - The transaction. - * @param {string} outputAddress - The output address. - * @param {networks.Network} network - The Bitcoin network. - * @returns {number} - The output index. - * @throws {Error} - If the matching output is not found. - */ -export declare const findMatchingTxOutputIndex: (tx: Transaction, outputAddress: string, network: networks.Network) => number; -/** - * Validate the staking transaction input data. - * - * @param {number} stakingAmountSat - The staking amount in satoshis. - * @param {number} timelock - The staking time in blocks. - * @param {StakingParams} params - The staking parameters. - * @param {UTXO[]} inputUTXOs - The input UTXOs. - * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte - * @throws {StakingError} - If the input data is invalid. - */ -export declare const validateStakingTxInputData: (stakingAmountSat: number, timelock: number, params: StakingParams, inputUTXOs: UTXO[], feeRate: number) => void; -/** - * Validate the staking parameters. - * Extend this method to add additional validation for staking parameters based - * on the staking type. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the parameters are invalid. - */ -export declare const validateParams: (params: StakingParams) => void; -/** - * Validate the staking timelock. - * - * @param {number} stakingTimelock - The staking timelock. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the staking timelock is invalid. - */ -export declare const validateStakingTimelock: (stakingTimelock: number, params: StakingParams) => void; -/** - * toBuffers converts an array of strings to an array of buffers. - * - * @param {string[]} inputs - The input strings. - * @returns {Buffer[]} - The buffers. - * @throws {StakingError} - If the values cannot be converted to buffers. - */ -export declare const toBuffers: (inputs: string[]) => Buffer[]; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js deleted file mode 100644 index 5c2b68e2a7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/index.js +++ /dev/null @@ -1,256 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.toBuffers = exports.validateStakingTimelock = exports.validateParams = exports.validateStakingTxInputData = exports.findMatchingTxOutputIndex = exports.deriveSlashingOutput = exports.deriveUnbondingOutputInfo = exports.deriveStakingOutputInfo = exports.buildStakingTransactionOutputs = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -const internalPubkey_1 = require("../../constants/internalPubkey"); -const error_1 = require("../../error"); -const btc_1 = require("../btc"); -const unbonding_1 = require("../../constants/unbonding"); -/** - * Build the staking output for the transaction which contains p2tr output - * with staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @param {number} amount - The amount to stake. - * @returns {TransactionOutput[]} - The staking transaction outputs. - * @throws {Error} - If the staking output cannot be built. - */ -const buildStakingTransactionOutputs = (scripts, network, amount) => { - const stakingOutputInfo = (0, exports.deriveStakingOutputInfo)(scripts, network); - const transactionOutputs = [ - { - scriptPubKey: stakingOutputInfo.scriptPubKey, - value: amount, - }, - ]; - if (scripts.dataEmbedScript) { - // Add the data embed output to the transaction - transactionOutputs.push({ - scriptPubKey: scripts.dataEmbedScript, - value: 0, - }); - } - return transactionOutputs; -}; -exports.buildStakingTransactionOutputs = buildStakingTransactionOutputs; -/** - * Derive the staking output address from the staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @returns {StakingOutput} - The staking output address and scriptPubKey. - * @throws {StakingError} - If the staking output address cannot be derived. - */ -const deriveStakingOutputInfo = (scripts, network) => { - // Build outputs - const scriptTree = [ - { - output: scripts.slashingScript, - }, - [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }], - ]; - // Create an pay-2-taproot (p2tr) output using the staking script - const stakingOutput = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree, - network, - }); - if (!stakingOutput.address) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build staking output"); - } - return { - outputAddress: stakingOutput.address, - scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(stakingOutput.address, network), - }; -}; -exports.deriveStakingOutputInfo = deriveStakingOutputInfo; -/** - * Derive the unbonding output address and scriptPubKey from the staking scripts. - * - * @param {StakingScripts} scripts - The staking scripts. - * @param {networks.Network} network - The Bitcoin network. - * @returns {OutputInfo} - The unbonding output address and scriptPubKey. - * @throws {StakingError} - If the unbonding output address cannot be derived. - */ -const deriveUnbondingOutputInfo = (scripts, network) => { - const outputScriptTree = [ - { - output: scripts.slashingScript, - }, - { output: scripts.unbondingTimelockScript }, - ]; - const unbondingOutput = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree: outputScriptTree, - network, - }); - if (!unbondingOutput.address) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build unbonding output"); - } - return { - outputAddress: unbondingOutput.address, - scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(unbondingOutput.address, network), - }; -}; -exports.deriveUnbondingOutputInfo = deriveUnbondingOutputInfo; -/** - * Derive the slashing output address and scriptPubKey from the staking scripts. - * - * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the - * unbonding timelock script as the timelock of the slashing transaction. - * This is due to slashing tx timelock is the same as the unbonding timelock. - * @param {networks.Network} network - The Bitcoin network. - * @returns {OutputInfo} - The slashing output address and scriptPubKey. - * @throws {StakingError} - If the slashing output address cannot be derived. - */ -const deriveSlashingOutput = (scripts, network) => { - const slashingOutput = bitcoinjs_lib_1.payments.p2tr({ - internalPubkey: internalPubkey_1.internalPubkey, - scriptTree: { output: scripts.unbondingTimelockScript }, - network, - }); - const slashingOutputAddress = slashingOutput.address; - if (!slashingOutputAddress) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, "Failed to build slashing output address"); - } - return { - outputAddress: slashingOutputAddress, - scriptPubKey: bitcoinjs_lib_1.address.toOutputScript(slashingOutputAddress, network), - }; -}; -exports.deriveSlashingOutput = deriveSlashingOutput; -/** - * Find the matching output index for the given transaction. - * - * @param {Transaction} tx - The transaction. - * @param {string} outputAddress - The output address. - * @param {networks.Network} network - The Bitcoin network. - * @returns {number} - The output index. - * @throws {Error} - If the matching output is not found. - */ -const findMatchingTxOutputIndex = (tx, outputAddress, network) => { - const index = tx.outs.findIndex(output => { - return bitcoinjs_lib_1.address.fromOutputScript(output.script, network) === outputAddress; - }); - if (index === -1) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_OUTPUT, `Matching output not found for address: ${outputAddress}`); - } - return index; -}; -exports.findMatchingTxOutputIndex = findMatchingTxOutputIndex; -/** - * Validate the staking transaction input data. - * - * @param {number} stakingAmountSat - The staking amount in satoshis. - * @param {number} timelock - The staking time in blocks. - * @param {StakingParams} params - The staking parameters. - * @param {UTXO[]} inputUTXOs - The input UTXOs. - * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte - * @throws {StakingError} - If the input data is invalid. - */ -const validateStakingTxInputData = (stakingAmountSat, timelock, params, inputUTXOs, feeRate) => { - if (stakingAmountSat < params.minStakingAmountSat || - stakingAmountSat > params.maxStakingAmountSat) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid staking amount"); - } - if (timelock < params.minStakingTimeBlocks || - timelock > params.maxStakingTimeBlocks) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid timelock"); - } - if (inputUTXOs.length == 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "No input UTXOs provided"); - } - if (feeRate <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Invalid fee rate"); - } -}; -exports.validateStakingTxInputData = validateStakingTxInputData; -/** - * Validate the staking parameters. - * Extend this method to add additional validation for staking parameters based - * on the staking type. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the parameters are invalid. - */ -const validateParams = (params) => { - // Check covenant public keys - if (params.covenantNoCoordPks.length == 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Could not find any covenant public keys"); - } - if (params.covenantNoCoordPks.length < params.covenantQuorum) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant public keys must be greater than or equal to the quorum"); - } - params.covenantNoCoordPks.forEach((pk) => { - if (!(0, btc_1.isValidNoCoordPublicKey)(pk)) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant public key should contains no coordinate"); - } - }); - // Check other parameters - if (params.unbondingTime <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Unbonding time must be greater than 0"); - } - if (params.unbondingFeeSat <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Unbonding fee must be greater than 0"); - } - if (params.maxStakingAmountSat < params.minStakingAmountSat) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Max staking amount must be greater or equal to min staking amount"); - } - if (params.minStakingAmountSat < params.unbondingFeeSat + unbonding_1.MIN_UNBONDING_OUTPUT_VALUE) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, `Min staking amount must be greater than unbonding fee plus ${unbonding_1.MIN_UNBONDING_OUTPUT_VALUE}`); - } - if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Max staking time must be greater or equal to min staking time"); - } - if (params.minStakingTimeBlocks <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Min staking time must be greater than 0"); - } - if (params.covenantQuorum <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Covenant quorum must be greater than 0"); - } - if (params.slashing) { - if (params.slashing.slashingRate <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing rate must be greater than 0"); - } - if (params.slashing.slashingRate > 1) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing rate must be less or equal to 1"); - } - if (params.slashing.slashingPkScriptHex.length == 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Slashing public key script is missing"); - } - if (params.slashing.minSlashingTxFeeSat <= 0) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_PARAMS, "Minimum slashing transaction fee must be greater than 0"); - } - } -}; -exports.validateParams = validateParams; -/** - * Validate the staking timelock. - * - * @param {number} stakingTimelock - The staking timelock. - * @param {StakingParams} params - The staking parameters. - * @throws {StakingError} - If the staking timelock is invalid. - */ -const validateStakingTimelock = (stakingTimelock, params) => { - if (stakingTimelock < params.minStakingTimeBlocks || - stakingTimelock > params.maxStakingTimeBlocks) { - throw new error_1.StakingError(error_1.StakingErrorCode.INVALID_INPUT, "Staking transaction timelock is out of range"); - } -}; -exports.validateStakingTimelock = validateStakingTimelock; -/** - * toBuffers converts an array of strings to an array of buffers. - * - * @param {string[]} inputs - The input strings. - * @returns {Buffer[]} - The buffers. - * @throws {StakingError} - If the values cannot be converted to buffers. - */ -const toBuffers = (inputs) => { - try { - return inputs.map((i) => Buffer.from(i, "hex")); - } - catch (error) { - throw error_1.StakingError.fromUnknown(error, error_1.StakingErrorCode.INVALID_INPUT, "Cannot convert values to buffers"); - } -}; -exports.toBuffers = toBuffers; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts deleted file mode 100644 index 519466725d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { StakingParams, VersionedStakingParams } from "../../types/params"; -export declare const getBabylonParamByBtcHeight: (height: number, babylonParamsVersions: VersionedStakingParams[]) => StakingParams; -export declare const getBabylonParamByVersion: (version: number, babylonParams: VersionedStakingParams[]) => StakingParams; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js deleted file mode 100644 index 1e8bff47cc..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/staking/param.js +++ /dev/null @@ -1,32 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getBabylonParamByVersion = exports.getBabylonParamByBtcHeight = void 0; -/* - Get the Babylon params version by BTC height - @param height - The BTC height - @param babylonParamsVersions - The Babylon params versions - @returns The Babylon params -*/ -const getBabylonParamByBtcHeight = (height, babylonParamsVersions) => { - // Sort by btcActivationHeight in ascending order - const sortedParams = [...babylonParamsVersions].sort((a, b) => b.btcActivationHeight - a.btcActivationHeight); - // Find first params where height is >= btcActivationHeight - const params = sortedParams.find((p) => height >= p.btcActivationHeight); - if (!params) - throw new Error(`Babylon params not found for height ${height}`); - return params; -}; -exports.getBabylonParamByBtcHeight = getBabylonParamByBtcHeight; -/* - Get the Babylon params by version - @param version - The Babylon params version - @param babylonParams - The Babylon params - @returns The Babylon params -*/ -const getBabylonParamByVersion = (version, babylonParams) => { - const params = babylonParams.find((p) => p.version === version); - if (!params) - throw new Error(`Babylon params not found for version ${version}`); - return params; -}; -exports.getBabylonParamByVersion = getBabylonParamByVersion; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts deleted file mode 100644 index de084faedf..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Input } from "bitcoinjs-lib/src/transaction"; -import { UTXO } from "../../types/UTXO"; -export declare const findInputUTXO: (inputUTXOs: UTXO[], input: Input) => UTXO; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js deleted file mode 100644 index 2b1057c246..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/findInputUTXO.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.findInputUTXO = void 0; -const btc_1 = require("../btc"); -const findInputUTXO = (inputUTXOs, input) => { - const inputUTXO = inputUTXOs.find((u) => (0, btc_1.transactionIdToHash)(u.txid).toString("hex") === - input.hash.toString("hex") && u.vout === input.index); - if (!inputUTXO) { - throw new Error(`Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString("hex")} ` + - `and vout: ${input.index}`); - } - return inputUTXO; -}; -exports.findInputUTXO = findInputUTXO; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts deleted file mode 100644 index 555848ace7..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PsbtInputExtended } from "bip174/src/lib/interfaces"; -import { UTXO } from "../../types"; -/** - * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type. - * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns - * the appropriate PSBT input fields required for that UTXO. - * - * @param {UTXO} utxo - The unspent transaction output to process - * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional). - * @returns {object} PSBT input fields object containing the necessary data - * @throws {Error} If required input data is missing or if an unsupported script type is provided - */ -export declare const getPsbtInputFields: (utxo: UTXO, publicKeyNoCoord?: Buffer) => PsbtInputExtended; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js deleted file mode 100644 index c68baee77d..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getPsbtInputFields.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getPsbtInputFields = void 0; -const getScriptType_1 = require("./getScriptType"); -/** - * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type. - * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns - * the appropriate PSBT input fields required for that UTXO. - * - * @param {UTXO} utxo - The unspent transaction output to process - * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional). - * @returns {object} PSBT input fields object containing the necessary data - * @throws {Error} If required input data is missing or if an unsupported script type is provided - */ -const getPsbtInputFields = (utxo, publicKeyNoCoord) => { - const scriptPubKey = Buffer.from(utxo.scriptPubKey, "hex"); - const type = (0, getScriptType_1.getScriptType)(scriptPubKey); - switch (type) { - case getScriptType_1.BitcoinScriptType.P2PKH: { - if (!utxo.rawTxHex) { - throw new Error("Missing rawTxHex for legacy P2PKH input"); - } - return { nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex") }; - } - case getScriptType_1.BitcoinScriptType.P2SH: { - if (!utxo.rawTxHex) { - throw new Error("Missing rawTxHex for P2SH input"); - } - if (!utxo.redeemScript) { - throw new Error("Missing redeemScript for P2SH input"); - } - return { - nonWitnessUtxo: Buffer.from(utxo.rawTxHex, "hex"), - redeemScript: Buffer.from(utxo.redeemScript, "hex"), - }; - } - case getScriptType_1.BitcoinScriptType.P2WPKH: { - return { - witnessUtxo: { - script: scriptPubKey, - value: utxo.value, - }, - }; - } - case getScriptType_1.BitcoinScriptType.P2WSH: { - if (!utxo.witnessScript) { - throw new Error("Missing witnessScript for P2WSH input"); - } - return { - witnessUtxo: { - script: scriptPubKey, - value: utxo.value, - }, - witnessScript: Buffer.from(utxo.witnessScript, "hex"), - }; - } - case getScriptType_1.BitcoinScriptType.P2TR: { - return Object.assign({ witnessUtxo: { - script: scriptPubKey, - value: utxo.value, - } }, (publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord })); - } - default: - throw new Error(`Unsupported script type: ${type}`); - } -}; -exports.getPsbtInputFields = getPsbtInputFields; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts deleted file mode 100644 index b5f970e920..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Supported Bitcoin script types - */ -export declare enum BitcoinScriptType { - P2PKH = "pubkeyhash", - P2SH = "scripthash", - P2WPKH = "witnesspubkeyhash", - P2WSH = "witnessscripthash", - P2TR = "taproot" -} -/** - * Determines the type of Bitcoin script. - * - * This function tries to parse the script using different Bitcoin payment types and returns - * a string identifier for the script type. - * - * @param script - The raw script as a Buffer - * @returns {BitcoinScriptType} The identified script type - * @throws {Error} If the script cannot be identified as any known type - */ -export declare const getScriptType: (script: Buffer) => BitcoinScriptType; diff --git a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js b/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js deleted file mode 100644 index 39934f5677..0000000000 --- a/modules/babylonlabs-io-btc-staking-ts/build/src/utils/utxo/getScriptType.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getScriptType = exports.BitcoinScriptType = void 0; -const bitcoinjs_lib_1 = require("bitcoinjs-lib"); -/** - * Supported Bitcoin script types - */ -var BitcoinScriptType; -(function (BitcoinScriptType) { - // Pay to Public Key Hash - BitcoinScriptType["P2PKH"] = "pubkeyhash"; - // Pay to Script Hash - BitcoinScriptType["P2SH"] = "scripthash"; - // Pay to Witness Public Key Hash - BitcoinScriptType["P2WPKH"] = "witnesspubkeyhash"; - // Pay to Witness Script Hash - BitcoinScriptType["P2WSH"] = "witnessscripthash"; - // Pay to Taproot - BitcoinScriptType["P2TR"] = "taproot"; -})(BitcoinScriptType || (exports.BitcoinScriptType = BitcoinScriptType = {})); -/** - * Determines the type of Bitcoin script. - * - * This function tries to parse the script using different Bitcoin payment types and returns - * a string identifier for the script type. - * - * @param script - The raw script as a Buffer - * @returns {BitcoinScriptType} The identified script type - * @throws {Error} If the script cannot be identified as any known type - */ -const getScriptType = (script) => { - try { - bitcoinjs_lib_1.payments.p2pkh({ output: script }); - return BitcoinScriptType.P2PKH; - } - catch (_a) { } - try { - bitcoinjs_lib_1.payments.p2sh({ output: script }); - return BitcoinScriptType.P2SH; - } - catch (_b) { } - try { - bitcoinjs_lib_1.payments.p2wpkh({ output: script }); - return BitcoinScriptType.P2WPKH; - } - catch (_c) { } - try { - bitcoinjs_lib_1.payments.p2wsh({ output: script }); - return BitcoinScriptType.P2WSH; - } - catch (_d) { } - try { - bitcoinjs_lib_1.payments.p2tr({ output: script }); - return BitcoinScriptType.P2TR; - } - catch (_e) { } - throw new Error("Unknown script type"); -}; -exports.getScriptType = getScriptType; diff --git a/modules/babylonlabs-io-btc-staking-ts/package.json b/modules/babylonlabs-io-btc-staking-ts/package.json index 9edd7116c2..b22a7dfcbc 100644 --- a/modules/babylonlabs-io-btc-staking-ts/package.json +++ b/modules/babylonlabs-io-btc-staking-ts/package.json @@ -1,6 +1,6 @@ { "name": "@bitgo/babylonlabs-io-btc-staking-ts", - "version": "2.5.7", + "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", diff --git a/modules/utxo-staking/package.json b/modules/utxo-staking/package.json index 514c0437bc..cfa91bc577 100644 --- a/modules/utxo-staking/package.json +++ b/modules/utxo-staking/package.json @@ -43,8 +43,8 @@ }, "type": "commonjs", "dependencies": { - "@babylonlabs-io/babylon-proto-ts": "1.0.0", - "@bitgo/babylonlabs-io-btc-staking-ts": "^2.4.1", + "@babylonlabs-io/babylon-proto-ts": "1.7.2", + "@bitgo/babylonlabs-io-btc-staking-ts": "^3.0.0", "@bitgo/utxo-core": "^1.20.3", "@bitgo/utxo-lib": "^11.11.0", "@bitgo/wasm-miniscript": "2.0.0-beta.7", diff --git a/modules/utxo-staking/src/babylon/delegationMessage.ts b/modules/utxo-staking/src/babylon/delegationMessage.ts index fd3fc40e1c..89368d5388 100644 --- a/modules/utxo-staking/src/babylon/delegationMessage.ts +++ b/modules/utxo-staking/src/babylon/delegationMessage.ts @@ -174,6 +174,14 @@ export function getBtcProviderForECKey( throw new Error(`unexpected signing step: ${options.action.name}`); } }, + + /** + * This function is only used by btc-staking-ts to create a staking expansion registration + * transaction, which we do not currently support. + */ + async getTransactionHex(txid: string): Promise { + throw new Error(`Unsupported operation getTransactionHex (txid=${txid})`); + }, }; } type Result = { @@ -252,6 +260,22 @@ export function toStakingTransaction(tx: TransactionLike): bitcoinjslib.Transact return bitcoinjslib.Transaction.fromHex(tx.toHex()); } +/** + * As of babylonlabs-io/btc-staking-ts v1.5.7, the BTC delegation message creation functions support two message types: + * - MsgCreateBTCDelegation + * - MsgBtcStakeExpand + * + * BitGo still only supports MsgCreateBTCDelegation, so we need to check the message type here. + * + * @param msg - the message to check + * @return `true` if the message is of type MsgCreateBTCDelegation + */ +function isMsgBtcStakeExpand( + msg: babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation | babylonProtobuf.btcstakingtx.MsgBtcStakeExpand +) { + return 'previousStakingTxHash' in msg; +} + /* * This is mostly lifted from * https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/staking/manager.ts#L100-L172 @@ -270,7 +294,7 @@ export async function createDelegationMessageWithTransaction( throw new Error('Invalid Babylon address'); } // Create delegation message without including inclusion proof - return manager.createBtcDelegationMsg( + const msg = await manager.createBtcDelegationMsg( channel, staking, { @@ -283,6 +307,14 @@ export async function createDelegationMessageWithTransaction( staking.stakerInfo, staking.params ); + + // It shouldn't be possible for us to create a MsgBtcStakeExpand here because that only gets created when + // we pass channel = delegation:expand into createBtcDelegationMsg, which we cannot do. + if (isMsgBtcStakeExpand(msg.value)) { + throw new Error('MsgBtcStakeExpand is not supported'); + } + + return { ...msg, value: msg.value as babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation }; } export async function createUnsignedPreStakeRegistrationBabylonTransactionWithBtcProvider( diff --git a/scripts/vendor-github-repo.ts b/scripts/vendor-github-repo.ts index b82754de94..e31b1e23e6 100644 --- a/scripts/vendor-github-repo.ts +++ b/scripts/vendor-github-repo.ts @@ -1,4 +1,4 @@ -import { execa, ResultPromise } from 'execa'; +import execa, { ExecaChildProcess } from 'execa'; import fs from 'fs/promises'; import tmp from 'tmp'; import yargs from 'yargs'; @@ -91,7 +91,7 @@ async function fetchArchive(lib: GithubSource, outfile: string): Promise { await fs.writeFile(outfile, Buffer.from(await result.arrayBuffer())); } -function pipe(cmd: ResultPromise): ResultPromise { +function pipe(cmd: ExecaChildProcess): ExecaChildProcess { cmd.stdout?.pipe(process.stdout); cmd.stderr?.pipe(process.stderr); return cmd; @@ -191,6 +191,30 @@ const vendorConfigs: VendorConfig[] = [ end: '8d84d9b02af73d7c216d87aceca3dec0baabfecf', }, }, + { + org: 'babylonlabs-io', + repo: 'btc-staking-ts', + tag: 'v2.5.7', + targetDir: 'modules/babylonlabs-io-btc-staking-ts', + removeFiles: [ + '.eslintrc.json', + '.github/', + '.husky/', + '.npmrc', + '.nvmrc', + '.prettierignore', + '.prettierrc.json', + 'docs/', + 'tests/', + '.releaserc.json', + '.commitlint.config.cjs', + 'README.md', + ], + cherryPick: { + start: '161a937c4303d8273922ebfd04640d2391aca246', + end: '8d84d9b02af73d7c216d87aceca3dec0baabfecf', + }, + }, ]; function getMatches(name: string, version: string | undefined): VendorConfig[] { diff --git a/yarn.lock b/yarn.lock index 3248be8426..c77c6fc1a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -909,6 +909,16 @@ dependencies: "@bufbuild/protobuf" "^2.2.0" +"@babylonlabs-io/babylon-proto-ts@1.7.2": + version "1.7.2" + resolved "https://registry.npmjs.org/@babylonlabs-io/babylon-proto-ts/-/babylon-proto-ts-1.7.2.tgz#7acbd0b38c7512216a10dc754d9354187f23de51" + integrity sha512-10WVqrXA9nIE8Pipmg0Y+ywT3ODonNoM1h3Hmu2oxc8F7IQBokx76WyLvVoY1Y7NxVuLm0uV9iEKZbp+YWwf9Q== + dependencies: + "@bufbuild/protobuf" "^2.2.0" + "@cosmjs/proto-signing" "^0.33.1" + "@cosmjs/stargate" "^0.33.1" + "@cosmjs/tendermint-rpc" "^0.33.1" + "@bitcoin-js/tiny-secp256k1-asmjs@2.2.3": version "2.2.3" resolved "https://registry.npmjs.org/@bitcoin-js/tiny-secp256k1-asmjs/-/tiny-secp256k1-asmjs-2.2.3.tgz" @@ -1249,6 +1259,16 @@ "@cosmjs/math" "^0.29.5" "@cosmjs/utils" "^0.29.5" +"@cosmjs/amino@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.33.1.tgz#0d4957b2e755af8392627c0c0f72bee129dcdcf3" + integrity sha512-WfWiBf2EbIWpwKG9AOcsIIkR717SY+JdlXM/SL/bI66BdrhniAF+/ZNis9Vo9HF6lP2UU5XrSmFA4snAvEgdrg== + dependencies: + "@cosmjs/crypto" "^0.33.1" + "@cosmjs/encoding" "^0.33.1" + "@cosmjs/math" "^0.33.1" + "@cosmjs/utils" "^0.33.1" + "@cosmjs/crypto@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.29.5.tgz" @@ -1275,6 +1295,19 @@ elliptic "^6.5.4" libsodium-wrappers "^0.7.6" +"@cosmjs/crypto@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.33.1.tgz#761b1623e4abe8af4cbf7ca92639561314f04c3b" + integrity sha512-U4kGIj/SNBzlb2FGgA0sMR0MapVgJUg8N+oIAiN5+vl4GZ3aefmoL1RDyTrFS/7HrB+M+MtHsxC0tvEu4ic/zA== + dependencies: + "@cosmjs/encoding" "^0.33.1" + "@cosmjs/math" "^0.33.1" + "@cosmjs/utils" "^0.33.1" + "@noble/hashes" "^1" + bn.js "^5.2.0" + elliptic "^6.6.1" + libsodium-wrappers-sumo "^0.7.11" + "@cosmjs/encoding@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.29.5.tgz" @@ -1293,7 +1326,7 @@ bech32 "^1.1.4" readonly-date "^1.0.0" -"@cosmjs/encoding@^0.33.0": +"@cosmjs/encoding@^0.33.0", "@cosmjs/encoding@^0.33.1": version "0.33.1" resolved "https://registry.npmjs.org/@cosmjs/encoding/-/encoding-0.33.1.tgz" integrity sha512-nuNxf29fUcQE14+1p//VVQDwd1iau5lhaW/7uMz7V2AH3GJbFJoJVaKvVyZvdFk+Cnu+s3wCqgq4gJkhRCJfKw== @@ -1310,6 +1343,14 @@ "@cosmjs/stream" "^0.29.5" xstream "^11.14.0" +"@cosmjs/json-rpc@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/json-rpc/-/json-rpc-0.33.1.tgz#a5b8459605750fa7d38c05aa6009a92010c0d042" + integrity sha512-T6VtWzecpmuTuMRGZWuBYHsMF/aznWCYUt/cGMWNSz7DBPipVd0w774PKpxXzpEbyt5sr61NiuLXc+Az15S/Cw== + dependencies: + "@cosmjs/stream" "^0.33.1" + xstream "^11.14.0" + "@cosmjs/math@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/math/-/math-0.29.5.tgz" @@ -1324,6 +1365,13 @@ dependencies: bn.js "^5.2.0" +"@cosmjs/math@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/math/-/math-0.33.1.tgz#04ae4cfdb05f04f1b13e908f9551ca85b13ba4d4" + integrity sha512-ytGkWdKFCPiiBU5eqjHNd59djPpIsOjbr2CkNjlnI1Zmdj+HDkSoD9MUGpz9/RJvRir5IvsXqdE05x8EtoQkJA== + dependencies: + bn.js "^5.2.0" + "@cosmjs/proto-signing@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.29.5.tgz" @@ -1337,6 +1385,18 @@ cosmjs-types "^0.5.2" long "^4.0.0" +"@cosmjs/proto-signing@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/proto-signing/-/proto-signing-0.33.1.tgz#b084eb86410486cff30da7de34a636421db90ca8" + integrity sha512-Sv4W+MxX+0LVnd+2rU4Fw1HRsmMwSVSYULj7pRkij3wnPwUlTVoJjmKFgKz13ooIlfzPrz/dnNjGp/xnmXChFQ== + dependencies: + "@cosmjs/amino" "^0.33.1" + "@cosmjs/crypto" "^0.33.1" + "@cosmjs/encoding" "^0.33.1" + "@cosmjs/math" "^0.33.1" + "@cosmjs/utils" "^0.33.1" + cosmjs-types "^0.9.0" + "@cosmjs/socket@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.29.5.tgz" @@ -1347,6 +1407,16 @@ ws "^7" xstream "^11.14.0" +"@cosmjs/socket@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/socket/-/socket-0.33.1.tgz#2402487e7c70c8a5c801bd3189a58a09da786513" + integrity sha512-KzAeorten6Vn20sMiM6NNWfgc7jbyVo4Zmxev1FXa5EaoLCZy48cmT3hJxUJQvJP/lAy8wPGEjZ/u4rmF11x9A== + dependencies: + "@cosmjs/stream" "^0.33.1" + isomorphic-ws "^4.0.1" + ws "^7" + xstream "^11.14.0" + "@cosmjs/stargate@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.29.5.tgz" @@ -1365,6 +1435,20 @@ protobufjs "~6.11.3" xstream "^11.14.0" +"@cosmjs/stargate@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/stargate/-/stargate-0.33.1.tgz#13972f710942ac728474051be4f9754814ccfb52" + integrity sha512-CnJ1zpSiaZgkvhk+9aTp5IPmgWn2uo+cNEBN8VuD9sD6BA0V4DMjqe251cNFLiMhkGtiE5I/WXFERbLPww3k8g== + dependencies: + "@cosmjs/amino" "^0.33.1" + "@cosmjs/encoding" "^0.33.1" + "@cosmjs/math" "^0.33.1" + "@cosmjs/proto-signing" "^0.33.1" + "@cosmjs/stream" "^0.33.1" + "@cosmjs/tendermint-rpc" "^0.33.1" + "@cosmjs/utils" "^0.33.1" + cosmjs-types "^0.9.0" + "@cosmjs/stream@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.29.5.tgz" @@ -1372,6 +1456,13 @@ dependencies: xstream "^11.14.0" +"@cosmjs/stream@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/stream/-/stream-0.33.1.tgz#2e928eb68c52253e64ab56a3047cd8039b66abde" + integrity sha512-bMUvEENjeQPSTx+YRzVsWT1uFIdHRcf4brsc14SOoRQ/j5rOJM/aHfsf/BmdSAnYbdOQ3CMKj/8nGAQ7xUdn7w== + dependencies: + xstream "^11.14.0" + "@cosmjs/tendermint-rpc@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.29.5.tgz" @@ -1388,6 +1479,22 @@ readonly-date "^1.0.0" xstream "^11.14.0" +"@cosmjs/tendermint-rpc@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.33.1.tgz#5ab5b0b63e585badaa5827aef7e9e3d18695630a" + integrity sha512-22klDFq2MWnf//C8+rZ5/dYatr6jeGT+BmVbutXYfAK9fmODbtFcumyvB6uWaEORWfNukl8YK1OLuaWezoQvxA== + dependencies: + "@cosmjs/crypto" "^0.33.1" + "@cosmjs/encoding" "^0.33.1" + "@cosmjs/json-rpc" "^0.33.1" + "@cosmjs/math" "^0.33.1" + "@cosmjs/socket" "^0.33.1" + "@cosmjs/stream" "^0.33.1" + "@cosmjs/utils" "^0.33.1" + axios "^1.6.0" + readonly-date "^1.0.0" + xstream "^11.14.0" + "@cosmjs/utils@^0.29.5": version "0.29.5" resolved "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.29.5.tgz" @@ -1398,6 +1505,11 @@ resolved "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.30.1.tgz" integrity sha512-KvvX58MGMWh7xA+N+deCfunkA/ZNDvFLw4YbOmX3f/XBIkqrVY7qlotfy2aNb1kgp6h4B6Yc8YawJPDTfvWX7g== +"@cosmjs/utils@^0.33.1": + version "0.33.1" + resolved "https://registry.npmjs.org/@cosmjs/utils/-/utils-0.33.1.tgz#8882cd26172cb5b0b692c179407d6c3904493fed" + integrity sha512-UnLHDY6KMmC+UXf3Ufyh+onE19xzEXjT4VZ504Acmk4PXxqyvG4cCPprlKUFnGUX7f0z8Or9MAOHXBx41uHBcg== + "@csstools/postcss-cascade-layers@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz" @@ -7337,7 +7449,7 @@ aws4@^1.8.0: resolved "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz" integrity sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw== -axios@0.25.0, axios@0.27.2, axios@1.7.4, axios@^0.21.2, axios@^0.26.1, axios@^1.12.0, axios@^1.8.3: +axios@0.25.0, axios@0.27.2, axios@1.7.4, axios@^0.21.2, axios@^0.26.1, axios@^1.12.0, axios@^1.6.0, axios@^1.8.3: version "1.12.1" resolved "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz#0747b39c5b615f81f93f2c138e6d82a71426937f" integrity sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ== @@ -9159,6 +9271,11 @@ cosmjs-types@^0.6.1: long "^4.0.0" protobufjs "~6.11.2" +cosmjs-types@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/cosmjs-types/-/cosmjs-types-0.9.0.tgz#c3bc482d28c7dfa25d1445093fdb2d9da1f6cfcc" + integrity sha512-MN/yUe6mkJwHnCFfsNPeCfXVhyxHYW6c/xDUzrSbBycYzw++XvWDMJArXp2pLdgD6FQ8DW79vkPjeNKVrXaHeQ== + crc-32@^1.2.0: version "1.2.2" resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz" @@ -14330,7 +14447,7 @@ libsodium-sumo@^0.7.15: resolved "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz" integrity sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw== -libsodium-wrappers-sumo@^0.7.9: +libsodium-wrappers-sumo@^0.7.11, libsodium-wrappers-sumo@^0.7.9: version "0.7.15" resolved "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz" integrity sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==