UNPKG

@babylonlabs-io/btc-staking-ts

Version:

Library exposing methods for the creation and consumption of Bitcoin transactions pertaining to Babylon's Bitcoin Staking protocol.

4 lines 254 kB
{ "version": 3, "sources": ["../src/index.ts", "../src/error/index.ts", "../src/utils/btc.ts", "../src/constants/keys.ts", "../src/utils/staking/index.ts", "../src/constants/internalPubkey.ts", "../src/constants/unbonding.ts", "../src/utils/babylon.ts", "../src/utils/staking/validation.ts", "../src/staking/psbt.ts", "../src/constants/transaction.ts", "../src/utils/utxo/findInputUTXO.ts", "../src/utils/utxo/getPsbtInputFields.ts", "../src/utils/utxo/getScriptType.ts", "../src/staking/stakingScript.ts", "../src/staking/transactions.ts", "../src/constants/dustSat.ts", "../src/constants/psbt.ts", "../src/utils/fee/index.ts", "../src/constants/fee.ts", "../src/utils/fee/utils.ts", "../src/staking/index.ts", "../src/staking/manager.ts", "../src/constants/registry.ts", "../src/utils/index.ts", "../src/utils/pop.ts", "../src/constants/staking.ts", "../src/utils/psbt/validateSignedPsbt.ts", "../src/utils/staking/param.ts", "../src/staking/observable/observableStakingScript.ts", "../src/staking/observable/index.ts", "../src/types/params.ts"], "sourcesContent": ["export { Staking, StakingScriptData } from \"./staking\";\nexport type { StakingScripts } from \"./staking\";\nexport * from \"./staking/manager\";\nexport {\n ObservableStaking,\n ObservableStakingScriptData,\n} from \"./staking/observable\";\nexport * from \"./staking/transactions\";\nexport * from \"./types\";\nexport * from \"./utils/btc\";\nexport {\n getBabylonParamByBtcHeight,\n getBabylonParamByVersion,\n} from \"./utils/staking/param\";\nexport * from \"./utils/utxo/findInputUTXO\";\nexport * from \"./utils/utxo/getPsbtInputFields\";\nexport * from \"./utils/utxo/getScriptType\";\n", "export enum StakingErrorCode {\n UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n INVALID_INPUT = \"INVALID_INPUT\",\n INVALID_OUTPUT = \"INVALID_OUTPUT\",\n SCRIPT_FAILURE = \"SCRIPT_FAILURE\",\n BUILD_TRANSACTION_FAILURE = \"BUILD_TRANSACTION_FAILURE\",\n INVALID_PARAMS = \"INVALID_PARAMS\",\n}\n\nexport class StakingError extends Error {\n public code: StakingErrorCode;\n constructor(code: StakingErrorCode, message?: string) {\n super(message);\n this.code = code;\n }\n\n // Static method to safely handle unknown errors\n static fromUnknown(\n error: unknown,\n code: StakingErrorCode,\n fallbackMsg?: string,\n ): StakingError {\n if (error instanceof StakingError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new StakingError(code, error.message);\n }\n return new StakingError(code, fallbackMsg);\n }\n}\n", "import * as ecc from \"@bitcoin-js/tiny-secp256k1-asmjs\";\nimport { address, initEccLib, networks } from \"bitcoinjs-lib\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\n\n// Initialize elliptic curve library\nexport const initBTCCurve = () => {\n initEccLib(ecc);\n};\n\n/**\n * Check whether the given address is a valid Bitcoin address.\n *\n * @param {string} btcAddress - The Bitcoin address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is valid, otherwise false.\n */\nexport const isValidBitcoinAddress = (\n btcAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n return !!address.toOutputScript(btcAddress, network);\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given address is a Taproot address.\n *\n * @param {string} taprootAddress - The Bitcoin bech32 encoded address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is a Taproot address, otherwise false.\n */\nexport const isTaproot = (\n taprootAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n const decoded = address.fromBech32(taprootAddress);\n if (decoded.version !== 1) {\n return false;\n }\n\n // Compare network properties instead of object reference\n // The bech32 is hardcoded in the bitcoinjs-lib library.\n // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36\n if (network.bech32 === networks.bitcoin.bech32) {\n // Check if address starts with \"bc1p\"\n return taprootAddress.startsWith(\"bc1p\");\n } else if (network.bech32 === networks.testnet.bech32) {\n // signet, regtest and testnet taproot addresses start with \"tb1p\" or \"sb1p\"\n return (\n taprootAddress.startsWith(\"tb1p\") || taprootAddress.startsWith(\"sb1p\")\n );\n }\n return false;\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given address is a Native SegWit address.\n *\n * @param {string} segwitAddress - The Bitcoin bech32 encoded address to check.\n * @param {object} network - The Bitcoin network (e.g., bitcoin.networks.bitcoin).\n * @returns {boolean} - True if the address is a Native SegWit address, otherwise false.\n */\nexport const isNativeSegwit = (\n segwitAddress: string,\n network: networks.Network,\n): boolean => {\n try {\n const decoded = address.fromBech32(segwitAddress);\n if (decoded.version !== 0) {\n return false;\n }\n\n // Compare network properties instead of object reference\n // The bech32 is hardcoded in the bitcoinjs-lib library.\n // https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/networks.ts#L36\n if (network.bech32 === networks.bitcoin.bech32) {\n // Check if address starts with \"bc1q\"\n return segwitAddress.startsWith(\"bc1q\");\n } else if (network.bech32 === networks.testnet.bech32) {\n // testnet native segwit addresses start with \"tb1q\"\n return segwitAddress.startsWith(\"tb1q\");\n }\n return false;\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Check whether the given public key is a valid public key without a coordinate.\n *\n * @param {string} pkWithNoCoord - public key without the coordinate.\n * @returns {boolean} - True if the public key without the coordinate is valid, otherwise false.\n */\nexport const isValidNoCoordPublicKey = (pkWithNoCoord: string): boolean => {\n try {\n const keyBuffer = Buffer.from(pkWithNoCoord, \"hex\");\n return validateNoCoordPublicKeyBuffer(keyBuffer);\n } catch (error) {\n return false;\n }\n};\n\n/**\n * Get the public key without the coordinate.\n *\n * @param {string} pkHex - The public key in hex, with or without the coordinate.\n * @returns {string} - The public key without the coordinate in hex.\n * @throws {Error} - If the public key is invalid.\n */\nexport const getPublicKeyNoCoord = (pkHex: string): string => {\n const publicKey = Buffer.from(pkHex, \"hex\");\n\n const publicKeyNoCoordBuffer =\n publicKey.length === NO_COORD_PK_BYTE_LENGTH\n ? publicKey\n : publicKey.subarray(1, 33);\n\n // Validate the public key without coordinate\n if (!validateNoCoordPublicKeyBuffer(publicKeyNoCoordBuffer)) {\n throw new Error(\"Invalid public key without coordinate\");\n }\n\n return publicKeyNoCoordBuffer.toString(\"hex\");\n};\n\nconst validateNoCoordPublicKeyBuffer = (pkBuffer: Buffer): boolean => {\n if (pkBuffer.length !== NO_COORD_PK_BYTE_LENGTH) {\n return false;\n }\n\n // Try both compressed forms: y-coordinate even (0x02) and y-coordinate odd (0x03)\n const compressedKeyEven = Buffer.concat([Buffer.from([0x02]), pkBuffer]);\n const compressedKeyOdd = Buffer.concat([Buffer.from([0x03]), pkBuffer]);\n\n return ecc.isPoint(compressedKeyEven) || ecc.isPoint(compressedKeyOdd);\n};\n\n/**\n * Convert a transaction id to a hash. in buffer format.\n *\n * @param {string} txId - The transaction id.\n * @returns {Buffer} - The transaction hash.\n */\nexport const transactionIdToHash = (txId: string): Buffer => {\n if (txId === \"\") {\n throw new Error(\"Transaction id cannot be empty\");\n }\n return Buffer.from(txId, \"hex\").reverse();\n};\n", "// NO_COORD_PK_BYTE_LENGTH is the length of a BTC public key without the coordinate in bytes.\nexport const NO_COORD_PK_BYTE_LENGTH = 32;\n", "import { address, networks, payments, Transaction } from \"bitcoinjs-lib\";\nimport { Taptree } from \"bitcoinjs-lib/src/types\";\nimport { internalPubkey } from \"../../constants/internalPubkey\";\nimport { StakingError, StakingErrorCode } from \"../../error\";\nimport { TransactionOutput } from \"../../types/psbtOutputs\";\nexport interface OutputInfo {\n scriptPubKey: Buffer;\n outputAddress: string;\n}\n\n/**\n * Build the staking output for the transaction which contains p2tr output\n * with staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @param {number} amount - The amount to stake.\n * @returns {TransactionOutput[]} - The staking transaction outputs.\n * @throws {Error} - If the staking output cannot be built.\n */\nexport const buildStakingTransactionOutputs = (\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n dataEmbedScript?: Buffer;\n },\n network: networks.Network,\n amount: number,\n): TransactionOutput[] => {\n const stakingOutputInfo = deriveStakingOutputInfo(scripts, network);\n const transactionOutputs: { scriptPubKey: Buffer; value: number }[] = [\n {\n scriptPubKey: stakingOutputInfo.scriptPubKey,\n value: amount,\n },\n ];\n if (scripts.dataEmbedScript) {\n // Add the data embed output to the transaction\n transactionOutputs.push({\n scriptPubKey: scripts.dataEmbedScript,\n value: 0,\n });\n }\n return transactionOutputs;\n};\n\n/**\n * Derive the staking output address from the staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {StakingOutput} - The staking output address and scriptPubKey.\n * @throws {StakingError} - If the staking output address cannot be derived.\n */\nexport const deriveStakingOutputInfo = (\n scripts: {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n },\n network: networks.Network,\n) => {\n // Build outputs\n const scriptTree: Taptree = [\n {\n output: scripts.slashingScript,\n },\n [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }],\n ];\n\n // Create an pay-2-taproot (p2tr) output using the staking script\n const stakingOutput = payments.p2tr({\n internalPubkey,\n scriptTree,\n network,\n });\n\n if (!stakingOutput.address) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build staking output\",\n );\n }\n\n return {\n outputAddress: stakingOutput.address,\n scriptPubKey: address.toOutputScript(stakingOutput.address, network),\n };\n};\n\n/**\n * Derive the unbonding output address and scriptPubKey from the staking scripts.\n *\n * @param {StakingScripts} scripts - The staking scripts.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {OutputInfo} - The unbonding output address and scriptPubKey.\n * @throws {StakingError} - If the unbonding output address cannot be derived.\n */\nexport const deriveUnbondingOutputInfo = (\n scripts: {\n unbondingTimelockScript: Buffer;\n slashingScript: Buffer;\n },\n network: networks.Network,\n) => {\n const outputScriptTree: Taptree = [\n {\n output: scripts.slashingScript,\n },\n { output: scripts.unbondingTimelockScript },\n ];\n\n const unbondingOutput = payments.p2tr({\n internalPubkey,\n scriptTree: outputScriptTree,\n network,\n });\n\n if (!unbondingOutput.address) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build unbonding output\",\n );\n }\n\n return {\n outputAddress: unbondingOutput.address,\n scriptPubKey: address.toOutputScript(unbondingOutput.address, network),\n };\n};\n\n/**\n * Derive the slashing output address and scriptPubKey from the staking scripts.\n *\n * @param {StakingScripts} scripts - The unbonding timelock scripts, we use the\n * unbonding timelock script as the timelock of the slashing transaction.\n * This is due to slashing tx timelock is the same as the unbonding timelock.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {OutputInfo} - The slashing output address and scriptPubKey.\n * @throws {StakingError} - If the slashing output address cannot be derived.\n */\nexport const deriveSlashingOutput = (\n scripts: {\n unbondingTimelockScript: Buffer;\n },\n network: networks.Network,\n) => {\n const slashingOutput = payments.p2tr({\n internalPubkey,\n scriptTree: { output: scripts.unbondingTimelockScript },\n network,\n });\n const slashingOutputAddress = slashingOutput.address;\n\n if (!slashingOutputAddress) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n \"Failed to build slashing output address\",\n );\n }\n\n return {\n outputAddress: slashingOutputAddress,\n scriptPubKey: address.toOutputScript(slashingOutputAddress, network),\n };\n};\n\n/**\n * Find the matching output index for the given transaction.\n *\n * @param {Transaction} tx - The transaction.\n * @param {string} outputAddress - The output address.\n * @param {networks.Network} network - The Bitcoin network.\n * @returns {number} - The output index.\n * @throws {Error} - If the matching output is not found.\n */\nexport const findMatchingTxOutputIndex = (\n tx: Transaction,\n outputAddress: string,\n network: networks.Network,\n) => {\n const index = tx.outs.findIndex((output) => {\n try {\n return address.fromOutputScript(output.script, network) === outputAddress;\n } catch (error) {\n return false;\n }\n });\n\n if (index === -1) {\n throw new StakingError(\n StakingErrorCode.INVALID_OUTPUT,\n `Matching output not found for address: ${outputAddress}`,\n );\n }\n\n return index;\n};\n\n/**\n * toBuffers converts an array of strings to an array of buffers.\n *\n * @param {string[]} inputs - The input strings.\n * @returns {Buffer[]} - The buffers.\n * @throws {StakingError} - If the values cannot be converted to buffers.\n */\nexport const toBuffers = (inputs: string[]): Buffer[] => {\n try {\n return inputs.map((i) => Buffer.from(i, \"hex\"));\n } catch (error) {\n throw StakingError.fromUnknown(\n error,\n StakingErrorCode.INVALID_INPUT,\n \"Cannot convert values to buffers\",\n );\n }\n};\n\n/**\n * Strips all signatures from a transaction by clearing both the script and\n * witness data. This is due to the fact that we only need the raw unsigned\n * transaction structure. The signatures are sent in a separate protobuf field\n * when creating the delegation message in the Babylon.\n * @param tx - The transaction to strip signatures from\n * @returns A copy of the transaction with all signatures removed\n */\nexport const clearTxSignatures = (tx: Transaction): Transaction => {\n tx.ins.forEach((input) => {\n input.script = Buffer.alloc(0);\n input.witness = [];\n });\n return tx;\n};\n\n/**\n * Derives the merkle proof from the list of hex strings. Note the\n * sibling hashes are reversed from hex before concatenation.\n * @param merkle - The merkle proof hex strings.\n * @returns The merkle proof in hex string format.\n */\nexport const deriveMerkleProof = (merkle: string[]) => {\n const proofHex = merkle.reduce((acc: string, m: string) => {\n return acc + Buffer.from(m, \"hex\").reverse().toString(\"hex\");\n }, \"\");\n return proofHex;\n};\n\n/**\n * Extracts the first valid Schnorr signature from a signed transaction.\n *\n * Since we only handle transactions with a single input and request a signature\n * for one public key, there can be at most one signature from the Bitcoin node.\n * A valid Schnorr signature is exactly 64 bytes in length.\n *\n * @param singedTransaction - The signed Bitcoin transaction to extract the signature from\n * @returns The first valid 64-byte Schnorr signature found in the transaction witness data,\n * or undefined if no valid signature exists\n */\nexport const extractFirstSchnorrSignatureFromTransaction = (\n singedTransaction: Transaction,\n): Buffer | undefined => {\n // Loop through each input to extract the witness signature\n for (const input of singedTransaction.ins) {\n if (input.witness && input.witness.length > 0) {\n const schnorrSignature = input.witness[0];\n\n // Check that it's a 64-byte Schnorr signature\n if (schnorrSignature.length === 64) {\n return schnorrSignature; // Return the first valid signature found\n }\n }\n }\n return undefined;\n};\n", "// internalPubkey denotes an unspendable internal public key to be used for the taproot output\nconst key =\n \"0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0\";\nexport const internalPubkey = Buffer.from(key, \"hex\").subarray(1, 33); // Do a subarray(1, 33) to get the public coordinate\n", "// minimum unbonding output value to avoid the unbonding output value being\n// less than Bitcoin dust\nexport const MIN_UNBONDING_OUTPUT_VALUE = 1000;\n", "import { fromBech32 } from \"@cosmjs/encoding\";\n\n/**\n * Validates a Babylon address. Babylon addresses are encoded in Bech32 format\n * and have a prefix of \"bbn\".\n * @param address - The address to validate.\n * @returns True if the address is valid, false otherwise.\n */\nexport const isValidBabylonAddress = (address: string): boolean => {\n try {\n const { prefix } = fromBech32(address);\n return prefix === \"bbn\";\n } catch (error) {\n return false;\n }\n};\n", "import { MIN_UNBONDING_OUTPUT_VALUE } from \"../../constants/unbonding\";\nimport { StakingError, StakingErrorCode } from \"../../error\";\nimport { StakingInputs, StakingParams, UTXO } from \"../../types\";\nimport { isValidBabylonAddress } from \"../babylon\";\nimport { isValidNoCoordPublicKey } from \"../btc\";\n\n/**\n * Validates that all finality providers from the previous staking are included\n * in the current staking expansion.\n * @param currentFinalityProviders - The finality providers in the expansion\n * @param previousFinalityProviders - The finality providers in the previous staking\n * @throws {StakingError} - If any previous finality providers are missing\n */\nexport const validateFinalityProviderSuperset = (\n currentFinalityProviders: string[],\n previousFinalityProviders: string[],\n) => {\n const missingPreviousFPs = previousFinalityProviders.filter(\n (prevFp) => !currentFinalityProviders.includes(prevFp),\n );\n\n if (missingPreviousFPs.length > 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Invalid staking expansion: all finality providers from the previous\n staking must be included. Missing: ${missingPreviousFPs.join(\", \")}`,\n );\n }\n};\n\n/**\n * Validates the staking expansion input\n * @param babylonBtcTipHeight - The Babylon BTC tip height\n * @param inputUTXOs - The input UTXOs\n * @param stakingInput - The staking input\n * @param previousStakingInput - The previous staking input\n * @param babylonAddress - The Babylon address\n * @returns true if validation passes, throws error if validation fails\n */\nexport const validateStakingExpansionInputs = ({\n babylonBtcTipHeight,\n inputUTXOs,\n stakingInput,\n previousStakingInput,\n babylonAddress,\n}: {\n babylonBtcTipHeight?: number;\n inputUTXOs: UTXO[];\n stakingInput: StakingInputs;\n previousStakingInput: StakingInputs;\n babylonAddress?: string;\n}) => {\n if (babylonBtcTipHeight === 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Babylon BTC tip height cannot be 0\",\n );\n }\n if (!inputUTXOs || inputUTXOs.length === 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"No input UTXOs provided\",\n );\n }\n if (babylonAddress && !isValidBabylonAddress(babylonAddress)) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Invalid Babylon address\",\n );\n }\n\n // TODO: We currently don't support increasing the staking amount\n if (stakingInput.stakingAmountSat !== previousStakingInput.stakingAmountSat) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Staking expansion amount must equal the previous staking amount\",\n );\n }\n\n // Validate that all previous finality providers are included in the expansion\n validateFinalityProviderSuperset(\n stakingInput.finalityProviderPksNoCoordHex,\n previousStakingInput.finalityProviderPksNoCoordHex,\n );\n};\n\n/**\n * Validate the staking transaction input data.\n *\n * @param {number} stakingAmountSat - The staking amount in satoshis.\n * @param {number} timelock - The staking time in blocks.\n * @param {StakingParams} params - The staking parameters.\n * @param {UTXO[]} inputUTXOs - The input UTXOs.\n * @param {number} feeRate - The Bitcoin fee rate in sat/vbyte\n * @throws {StakingError} - If the input data is invalid.\n */\nexport const validateStakingTxInputData = (\n stakingAmountSat: number,\n timelock: number,\n params: StakingParams,\n inputUTXOs: UTXO[],\n feeRate: number,\n) => {\n if (\n stakingAmountSat < params.minStakingAmountSat ||\n stakingAmountSat > params.maxStakingAmountSat\n ) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Invalid staking amount\",\n );\n }\n\n if (\n timelock < params.minStakingTimeBlocks ||\n timelock > params.maxStakingTimeBlocks\n ) {\n throw new StakingError(StakingErrorCode.INVALID_INPUT, \"Invalid timelock\");\n }\n\n if (inputUTXOs.length == 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"No input UTXOs provided\",\n );\n }\n if (feeRate <= 0) {\n throw new StakingError(StakingErrorCode.INVALID_INPUT, \"Invalid fee rate\");\n }\n};\n\n/**\n * Validate the staking parameters.\n * Extend this method to add additional validation for staking parameters based\n * on the staking type.\n * @param {StakingParams} params - The staking parameters.\n * @throws {StakingError} - If the parameters are invalid.\n */\nexport const validateParams = (params: StakingParams) => {\n // Check covenant public keys\n if (params.covenantNoCoordPks.length == 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Could not find any covenant public keys\",\n );\n }\n if (params.covenantNoCoordPks.length < params.covenantQuorum) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Covenant public keys must be greater than or equal to the quorum\",\n );\n }\n params.covenantNoCoordPks.forEach((pk) => {\n if (!isValidNoCoordPublicKey(pk)) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Covenant public key should contains no coordinate\",\n );\n }\n });\n // Check other parameters\n if (params.unbondingTime <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Unbonding time must be greater than 0\",\n );\n }\n if (params.unbondingFeeSat <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Unbonding fee must be greater than 0\",\n );\n }\n if (params.maxStakingAmountSat < params.minStakingAmountSat) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Max staking amount must be greater or equal to min staking amount\",\n );\n }\n if (\n params.minStakingAmountSat <\n params.unbondingFeeSat + MIN_UNBONDING_OUTPUT_VALUE\n ) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n `Min staking amount must be greater than unbonding fee plus ${MIN_UNBONDING_OUTPUT_VALUE}`,\n );\n }\n if (params.maxStakingTimeBlocks < params.minStakingTimeBlocks) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Max staking time must be greater or equal to min staking time\",\n );\n }\n if (params.minStakingTimeBlocks <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Min staking time must be greater than 0\",\n );\n }\n if (params.covenantQuorum <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Covenant quorum must be greater than 0\",\n );\n }\n if (params.slashing) {\n if (params.slashing.slashingRate <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Slashing rate must be greater than 0\",\n );\n }\n if (params.slashing.slashingRate > 1) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Slashing rate must be less or equal to 1\",\n );\n }\n if (params.slashing.slashingPkScriptHex.length == 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Slashing public key script is missing\",\n );\n }\n if (params.slashing.minSlashingTxFeeSat <= 0) {\n throw new StakingError(\n StakingErrorCode.INVALID_PARAMS,\n \"Minimum slashing transaction fee must be greater than 0\",\n );\n }\n }\n};\n\n/**\n * Validate the staking timelock.\n *\n * @param {number} stakingTimelock - The staking timelock.\n * @param {StakingParams} params - The staking parameters.\n * @throws {StakingError} - If the staking timelock is invalid.\n */\nexport const validateStakingTimelock = (\n stakingTimelock: number,\n params: StakingParams,\n) => {\n if (\n stakingTimelock < params.minStakingTimeBlocks ||\n stakingTimelock > params.maxStakingTimeBlocks\n ) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Staking transaction timelock is out of range\",\n );\n }\n};\n\n/**\n * Validate the staking expansion covenant quorum.\n *\n * The quorum is the number of covenant members that must be active in the\n * previous staking transaction in order to expand the staking.\n *\n * If the quorum is not met, the staking expansion will fail.\n *\n * @param {StakingParams} paramsForPreviousStakingTx - The staking parameters\n * for the previous staking transaction.\n * @param {StakingParams} paramsForCurrentStakingTx - The staking parameters\n * for the current staking transaction.\n * @throws {StakingError} - If the staking expansion covenant quorum is invalid.\n */\nexport const validateStakingExpansionCovenantQuorum = (\n paramsForPreviousStakingTx: StakingParams,\n paramsForCurrentStakingTx: StakingParams,\n) => {\n const previousCovenantMembers = paramsForPreviousStakingTx.covenantNoCoordPks;\n const currentCovenantMembers = paramsForCurrentStakingTx.covenantNoCoordPks;\n const requiredQuorum = paramsForPreviousStakingTx.covenantQuorum;\n\n // Count how many previous covenant members are still active\n const activePreviousMembers = previousCovenantMembers.filter((prevMember) =>\n currentCovenantMembers.includes(prevMember),\n ).length;\n\n if (activePreviousMembers < requiredQuorum) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Staking expansion failed: insufficient covenant quorum. ` +\n `Required: ${requiredQuorum}, Available: ${activePreviousMembers}. ` +\n `Too many covenant members have rotated out.`,\n );\n }\n};\n", "import { Psbt, Transaction, networks, payments } from \"bitcoinjs-lib\";\nimport { Taptree } from \"bitcoinjs-lib/src/types\";\nimport { internalPubkey } from \"../constants/internalPubkey\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\nimport { REDEEM_VERSION } from \"../constants/transaction\";\nimport { UTXO } from \"../types/UTXO\";\nimport { deriveUnbondingOutputInfo } from \"../utils/staking\";\nimport { findInputUTXO } from \"../utils/utxo/findInputUTXO\";\nimport { getPsbtInputFields } from \"../utils/utxo/getPsbtInputFields\";\nimport { BitcoinScriptType, getScriptType } from \"../utils/utxo/getScriptType\";\nimport { StakingScripts } from \"./stakingScript\";\n\n/**\n * Convert a staking transaction to a PSBT.\n *\n * @param {Transaction} stakingTx - The staking transaction to convert to PSBT.\n * @param {networks.Network} network - The network to use for the PSBT.\n * @param {UTXO[]} inputUTXOs - The UTXOs to be used as inputs for the staking\n * transaction.\n * @param {Buffer} [publicKeyNoCoord] - The public key of staker (optional)\n * @returns {Psbt} - The PSBT for the staking transaction.\n * @throws {Error} If unable to create PSBT from transaction\n */\nexport const stakingPsbt = (\n stakingTx: Transaction,\n network: networks.Network,\n inputUTXOs: UTXO[],\n publicKeyNoCoord?: Buffer,\n): Psbt => {\n if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {\n throw new Error(\"Invalid public key\");\n }\n\n const psbt = new Psbt({ network });\n\n if (stakingTx.version !== undefined) psbt.setVersion(stakingTx.version);\n if (stakingTx.locktime !== undefined) psbt.setLocktime(stakingTx.locktime);\n\n stakingTx.ins.forEach((input) => {\n const inputUTXO = findInputUTXO(inputUTXOs, input);\n const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);\n\n psbt.addInput({\n hash: input.hash,\n index: input.index,\n sequence: input.sequence,\n ...psbtInputData,\n });\n });\n\n stakingTx.outs.forEach((o) => {\n psbt.addOutput({ script: o.script, value: o.value });\n });\n\n return psbt;\n};\n\n/**\n * Convert a staking expansion transaction to a PSBT.\n *\n * @param {networks.Network} network - The Bitcoin network to use for the PSBT\n * @param {Transaction} stakingTx - The staking expansion transaction to convert\n * @param {Object} previousStakingTxInfo - Information about the previous staking transaction\n * @param {Transaction} previousStakingTxInfo.stakingTx - The previous staking transaction\n * @param {number} previousStakingTxInfo.outputIndex - The index of the staking output in the previous transaction\n * @param {UTXO[]} inputUTXOs - Available UTXOs for the funding input\n * @param {Buffer} [publicKeyNoCoord] - The staker's public key without coordinate (for Taproot)\n * @returns {Psbt} The PSBT for the staking expansion transaction\n * @throws {Error} If validation fails or required data is missing\n */\nexport const stakingExpansionPsbt = (\n network: networks.Network,\n stakingTx: Transaction,\n previousStakingTxInfo: {\n stakingTx: Transaction;\n outputIndex: number;\n },\n inputUTXOs: UTXO[],\n previousScripts: StakingScripts,\n publicKeyNoCoord?: Buffer,\n): Psbt => {\n // Initialize PSBT with the specified network\n const psbt = new Psbt({ network });\n\n // Set transaction version and locktime if provided\n if (stakingTx.version !== undefined) psbt.setVersion(stakingTx.version);\n if (stakingTx.locktime !== undefined) psbt.setLocktime(stakingTx.locktime);\n\n // Validate the public key format if provided\n if (publicKeyNoCoord && publicKeyNoCoord.length !== NO_COORD_PK_BYTE_LENGTH) {\n throw new Error(\"Invalid public key\");\n }\n\n // Extract the previous staking output from the previous staking transaction\n const previousStakingOutput =\n previousStakingTxInfo.stakingTx.outs[previousStakingTxInfo.outputIndex];\n if (!previousStakingOutput) {\n throw new Error(\"Previous staking output not found\");\n }\n\n // Validate that the previous staking output is a Taproot (P2TR) script\n if (getScriptType(previousStakingOutput.script) !== BitcoinScriptType.P2TR) {\n throw new Error(\"Previous staking output script type is not P2TR\");\n }\n\n // Validate that the staking expansion transaction has exactly 2 inputs\n // Input 0: Previous staking output (existing stake)\n // Input 1: Funding UTXO (additional funds for fees or staking amount)\n if (stakingTx.ins.length !== 2) {\n throw new Error(\"Staking expansion transaction must have exactly 2 inputs\");\n }\n\n // Validate the first input matches the previous staking transaction\n const txInputs = stakingTx.ins;\n\n // Check that the first input references the correct previous staking\n // transaction\n if (\n Buffer.from(txInputs[0].hash).reverse().toString(\"hex\") !==\n previousStakingTxInfo.stakingTx.getId()\n ) {\n throw new Error(\"Previous staking input hash does not match\");\n }\n // Check that the first input references the correct output index\n else if (txInputs[0].index !== previousStakingTxInfo.outputIndex) {\n throw new Error(\"Previous staking input index does not match\");\n }\n\n // Build input tapleaf script that spends the previous staking output\n const inputScriptTree: Taptree = [\n { output: previousScripts.slashingScript },\n [\n { output: previousScripts.unbondingScript },\n { output: previousScripts.timelockScript },\n ],\n ];\n const inputRedeem = {\n output: previousScripts.unbondingScript,\n redeemVersion: REDEEM_VERSION,\n };\n const p2tr = payments.p2tr({\n internalPubkey,\n scriptTree: inputScriptTree,\n redeem: inputRedeem,\n network,\n });\n\n if (!p2tr.witness || p2tr.witness.length === 0) {\n throw new Error(\n \"Failed to create P2TR witness for expansion transaction input\",\n );\n }\n\n const inputTapLeafScript = {\n leafVersion: inputRedeem.redeemVersion,\n script: inputRedeem.output,\n controlBlock: p2tr.witness[p2tr.witness.length - 1],\n };\n\n // Add the previous staking input to the PSBT\n // This input spends the existing staking output\n psbt.addInput({\n hash: txInputs[0].hash,\n index: txInputs[0].index,\n sequence: txInputs[0].sequence,\n witnessUtxo: {\n script: previousStakingOutput.script,\n value: previousStakingOutput.value,\n },\n tapInternalKey: internalPubkey,\n tapLeafScript: [inputTapLeafScript],\n });\n\n // Add the second input (funding UTXO) to the PSBT\n // This input provides additional funds for fees or staking amount\n const inputUTXO = findInputUTXO(inputUTXOs, txInputs[1]);\n const psbtInputData = getPsbtInputFields(inputUTXO, publicKeyNoCoord);\n\n psbt.addInput({\n hash: txInputs[1].hash,\n index: txInputs[1].index,\n sequence: txInputs[1].sequence,\n ...psbtInputData,\n });\n\n // Add all outputs from the staking expansion transaction to the PSBT\n stakingTx.outs.forEach((o) => {\n psbt.addOutput({ script: o.script, value: o.value });\n });\n\n return psbt;\n};\n\nexport const unbondingPsbt = (\n scripts: {\n unbondingScript: Buffer;\n timelockScript: Buffer;\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n },\n unbondingTx: Transaction,\n stakingTx: Transaction,\n network: networks.Network,\n unbondingFee: number,\n): Psbt => {\n if (unbondingTx.outs.length !== 1) {\n throw new Error(\"Unbonding transaction must have exactly one output\");\n }\n if (unbondingTx.ins.length !== 1) {\n throw new Error(\"Unbonding transaction must have exactly one input\");\n }\n\n validateUnbondingOutput(\n scripts,\n unbondingTx,\n stakingTx,\n network,\n unbondingFee,\n );\n\n const psbt = new Psbt({ network });\n\n if (unbondingTx.version !== undefined) {\n psbt.setVersion(unbondingTx.version);\n }\n if (unbondingTx.locktime !== undefined) {\n psbt.setLocktime(unbondingTx.locktime);\n }\n\n const input = unbondingTx.ins[0];\n const outputIndex = input.index;\n\n // Build input tapleaf script\n const inputScriptTree: Taptree = [\n { output: scripts.slashingScript },\n [{ output: scripts.unbondingScript }, { output: scripts.timelockScript }],\n ];\n\n // This is the tapleaf we are actually spending\n const inputRedeem = {\n output: scripts.unbondingScript,\n redeemVersion: REDEEM_VERSION,\n };\n\n // Create a P2TR payment that includes scriptTree + redeem\n const p2tr = payments.p2tr({\n internalPubkey,\n scriptTree: inputScriptTree,\n redeem: inputRedeem,\n network,\n });\n\n const inputTapLeafScript = {\n leafVersion: inputRedeem.redeemVersion,\n script: inputRedeem.output,\n controlBlock: p2tr.witness![p2tr.witness!.length - 1],\n };\n\n psbt.addInput({\n hash: input.hash,\n index: input.index,\n sequence: input.sequence,\n tapInternalKey: internalPubkey,\n witnessUtxo: {\n value: stakingTx.outs[outputIndex].value,\n script: stakingTx.outs[outputIndex].script,\n },\n tapLeafScript: [inputTapLeafScript],\n });\n\n psbt.addOutput({\n script: unbondingTx.outs[0].script,\n value: unbondingTx.outs[0].value,\n });\n\n return psbt;\n};\n\n/**\n * Validate the unbonding output for a given unbonding transaction.\n *\n * @param {Object} scripts - The scripts to use for the unbonding output.\n * @param {Transaction} unbondingTx - The unbonding transaction.\n * @param {Transaction} stakingTx - The staking transaction.\n * @param {networks.Network} network - The network to use for the unbonding output.\n * @param {number} unbondingFee - The expected unbonding fee.\n */\nconst validateUnbondingOutput = (\n scripts: {\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n },\n unbondingTx: Transaction,\n stakingTx: Transaction,\n network: networks.Network,\n unbondingFee: number,\n) => {\n const unbondingOutputInfo = deriveUnbondingOutputInfo(scripts, network);\n if (\n unbondingOutputInfo.scriptPubKey.toString(\"hex\") !==\n unbondingTx.outs[0].script.toString(\"hex\")\n ) {\n throw new Error(\n \"Unbonding output script does not match the expected\" +\n \" script while building psbt\",\n );\n }\n\n const stakingOutputIndex = unbondingTx.ins[0].index;\n const stakingOutputValue = stakingTx.outs[stakingOutputIndex].value;\n const expectedUnbondingValue = stakingOutputValue - unbondingFee;\n const actualUnbondingValue = unbondingTx.outs[0].value;\n\n if (actualUnbondingValue !== expectedUnbondingValue) {\n throw new Error(\n `Unbonding output value ${actualUnbondingValue} does not match expected value ${expectedUnbondingValue} ` +\n `(staking output: ${stakingOutputValue}, unbonding fee: ${unbondingFee})`,\n );\n }\n};\n", "export const REDEEM_VERSION = 192;\n", "import { Input } from \"bitcoinjs-lib/src/transaction\";\n\nimport { UTXO } from \"../../types/UTXO\";\nimport { transactionIdToHash } from \"../btc\";\n\nexport const findInputUTXO = (inputUTXOs: UTXO[], input: Input): UTXO => {\n const inputUTXO = inputUTXOs.find(\n (u) =>\n transactionIdToHash(u.txid).toString(\"hex\") ===\n input.hash.toString(\"hex\") && u.vout === input.index,\n );\n if (!inputUTXO) {\n throw new Error(\n `Input UTXO not found for txid: ${Buffer.from(input.hash).reverse().toString(\"hex\")} ` +\n `and vout: ${input.index}`,\n );\n }\n return inputUTXO;\n};\n", "import { PsbtInputExtended } from \"bip174/src/lib/interfaces\";\nimport * as bitcoin from \"bitcoinjs-lib\";\n\nimport { StakingError, StakingErrorCode } from \"../../error\";\nimport { UTXO } from \"../../types\";\nimport { BitcoinScriptType, getScriptType } from \"./getScriptType\";\n\n/**\n * Validates a raw transaction hex string against the provided UTXO data.\n * Decodes the transaction and verifies that the transaction ID, vout index,\n * scriptPubKey, and value all match the UTXO. Returns the validated raw\n * transaction as a buffer.\n */\nconst validateRawTxHex = (utxo: UTXO): Buffer => {\n if (!utxo.rawTxHex) {\n throw new StakingError(StakingErrorCode.INVALID_INPUT, \"Missing rawTxHex\");\n }\n const rawTxBuffer = Buffer.from(utxo.rawTxHex, \"hex\");\n\n let decodedTx: bitcoin.Transaction;\n try {\n decodedTx = bitcoin.Transaction.fromBuffer(rawTxBuffer);\n } catch (error) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Failed to decode rawTxHex: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n );\n }\n\n const txid = decodedTx.getId();\n if (txid !== utxo.txid) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Transaction ID mismatch: expected ${utxo.txid}, got ${txid}`,\n );\n }\n\n if (utxo.vout >= decodedTx.outs.length) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Invalid vout index: ${utxo.vout} out of ${decodedTx.outs.length} outputs`,\n );\n }\n\n const output = decodedTx.outs[utxo.vout];\n const actualScriptPubKey = output.script.toString(\"hex\");\n if (actualScriptPubKey !== utxo.scriptPubKey) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Script mismatch at vout ${utxo.vout}: expected ${utxo.scriptPubKey}, got ${actualScriptPubKey}`,\n );\n }\n\n if (output.value !== utxo.value) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Value mismatch at vout ${utxo.vout}: expected ${utxo.value}, got ${output.value}`,\n );\n }\n\n return rawTxBuffer;\n};\n\nconst validateRedeemScript = (utxo: UTXO): Buffer => {\n if (!utxo.redeemScript) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Missing redeemScript for P2SH input\",\n );\n }\n const scriptPubKey = Buffer.from(utxo.scriptPubKey, \"hex\");\n const redeemScriptBuffer = Buffer.from(utxo.redeemScript, \"hex\");\n // Compute RIPEMD160(SHA256(redeemScript)) to get the 20-byte hash used in P2SH addresses\n const redeemScriptHash = bitcoin.crypto.hash160(redeemScriptBuffer);\n // Create a P2SH payment object from the hash to reconstruct the scriptPubKey for validation\n const p2shPayment = bitcoin.payments.p2sh({ hash: redeemScriptHash });\n\n const expectedOutput = p2shPayment.output as Buffer;\n if (\n !expectedOutput ||\n scriptPubKey.toString(\"hex\") !== expectedOutput.toString(\"hex\")\n ) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Redeem script hash does not match P2SH scriptPubKey\",\n );\n }\n\n return redeemScriptBuffer;\n};\n\nconst validateWitnessScript = (utxo: UTXO): Buffer => {\n if (!utxo.witnessScript) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Missing witnessScript for P2WSH input\",\n );\n }\n const scriptPubKey = Buffer.from(utxo.scriptPubKey, \"hex\");\n const witnessScriptBuffer = Buffer.from(utxo.witnessScript, \"hex\");\n // Compute SHA256(witnessScript) to get the 32-byte hash used in P2WSH addresses\n const witnessScriptHash = bitcoin.crypto.sha256(witnessScriptBuffer);\n // Create a P2WSH payment object from the hash to reconstruct the scriptPubKey for validation\n const p2wshPayment = bitcoin.payments.p2wsh({ hash: witnessScriptHash });\n\n const expectedOutput = p2wshPayment.output as Buffer;\n if (\n !expectedOutput ||\n scriptPubKey.toString(\"hex\") !== expectedOutput.toString(\"hex\")\n ) {\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n \"Witness script hash does not match P2WSH scriptPubKey\",\n );\n }\n\n return witnessScriptBuffer;\n};\n\n/**\n * Determines and constructs the correct PSBT input fields for a given UTXO based on its script type.\n * This function handles different Bitcoin script types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR) and returns\n * the appropriate PSBT input fields required for that UTXO.\n *\n * @param {UTXO} utxo - The unspent transaction output to process\n * @param {Buffer} [publicKeyNoCoord] - The public of the staker (optional).\n * @returns {object} PSBT input fields object containing the necessary data\n * @throws {Error} If required input data is missing or if an unsupported script type is provided\n */\n\nexport const getPsbtInputFields = (\n utxo: UTXO,\n publicKeyNoCoord?: Buffer,\n): PsbtInputExtended => {\n const scriptPubKey = Buffer.from(utxo.scriptPubKey, \"hex\");\n const type = getScriptType(scriptPubKey);\n\n switch (type) {\n case BitcoinScriptType.P2PKH: {\n const nonWitnessUtxo = validateRawTxHex(utxo);\n return { nonWitnessUtxo };\n }\n case BitcoinScriptType.P2SH: {\n const nonWitnessUtxo = validateRawTxHex(utxo);\n const redeemScript = validateRedeemScript(utxo);\n return {\n nonWitnessUtxo,\n redeemScript,\n };\n }\n case BitcoinScriptType.P2WPKH: {\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n };\n }\n case BitcoinScriptType.P2WSH: {\n const witnessScript = validateWitnessScript(utxo);\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n witnessScript,\n };\n }\n case BitcoinScriptType.P2TR: {\n return {\n witnessUtxo: {\n script: scriptPubKey,\n value: utxo.value,\n },\n ...(publicKeyNoCoord && { tapInternalKey: publicKeyNoCoord }),\n };\n }\n default:\n throw new StakingError(\n StakingErrorCode.INVALID_INPUT,\n `Unsupported script type: ${type}`,\n );\n }\n};\n", "import { payments } from \"bitcoinjs-lib\";\n\n/**\n * Supported Bitcoin script types\n */\nexport enum BitcoinScriptType {\n // Pay to Public Key Hash\n P2PKH = \"pubkeyhash\",\n // Pay to Script Hash\n P2SH = \"scripthash\",\n // Pay to Witness Public Key Hash\n P2WPKH = \"witnesspubkeyhash\",\n // Pay to Witness Script Hash\n P2WSH = \"witnessscripthash\",\n // Pay to Taproot\n P2TR = \"taproot\",\n}\n\n/**\n * Determines the type of Bitcoin script.\n *\n * This function tries to parse the script using different Bitcoin payment types and returns\n * a string identifier for the script type.\n *\n * @param script - The raw script as a Buffer\n * @returns {BitcoinScriptType} The identified script type\n * @throws {Error} If the script cannot be identified as any known type\n */\n\nexport const getScriptType = (script: Buffer): BitcoinScriptType => {\n try {\n payments.p2pkh({ output: script });\n return BitcoinScriptType.P2PKH;\n } catch {}\n try {\n payments.p2sh({ output: script });\n return BitcoinScriptType.P2SH;\n } catch {}\n try {\n payments.p2wpkh({ output: script });\n return BitcoinScriptType.P2WPKH;\n } catch {}\n try {\n payments.p2wsh({ output: script });\n return BitcoinScriptType.P2WSH;\n } catch {}\n try {\n payments.p2tr({ output: script });\n return BitcoinScriptType.P2TR;\n } catch {}\n\n throw new Error(\"Unknown script type\");\n};\n", "import { opcodes, script } from \"bitcoinjs-lib\";\nimport { NO_COORD_PK_BYTE_LENGTH } from \"../constants/keys\";\n\nexport const MAGIC_BYTES_LEN = 4;\n\n// Represents the staking scripts used in BTC staking.\nexport interface StakingScripts {\n timelockScript: Buffer;\n unbondingScript: Buffer;\n slashingScript: Buffer;\n unbondingTimelockScript: Buffer;\n}\n\n// StakingScriptData is a class that holds the data required for the BTC Staking Script\n// and exposes methods for converting it into useful formats\nexport class StakingScriptData {\n stakerKey: Buffer;\n finalityProviderKeys: Buffer[];\n covenantKeys: Buffer[];\n covenantThreshold: number;\n stakingTimeLock: number;\n unbondingTimeLock: number;\n\n constructor(\n // The `stakerKey` is the public key of the staker without the coordinate bytes.\n stakerKey: Buffer,\n // A list of public keys without the coordinate bytes corresponding to the finality providers\n // the stake will be delegated to.\n // Currently, Babylon does not support restaking, so this should contain only a single item.\n finalityProviderKeys: Buffer[],\n // A list of the public keys without the coordinate bytes corresponding to\n // the covenant emulators.\n // This is a parameter of the Babylon system and should be retrieved from there.\n covenantKeys: Buffer[],\n // The number of covenant emulator signatures required for a transaction\n // to be valid.\n // This is a parameter of the Babylon system and should be retrieved from there.\n covenantThreshold: number,\n // The staking period denoted as a number of BTC blocks.\n stakingTimelock: number,\n // The unbonding period denoted as a number of BTC blocks.\n // This value should be more than equal than the minimum unbonding time of the\n // Babylon system.\n unbondingTimelock: number,\n ) {\n if (\n !stakerKey ||\n !finalityProviderKeys ||\n !covenantKeys ||\n !covenantThreshold ||\n !stakingTimelock ||\n !unbondingTimelock\n ) {\n throw new Error(\"Missing required input values\");\n }\n this.stakerKey = stakerKey;\n this.finalityProviderKeys = finalityProviderKeys;\n this.covenantKeys = covenantKeys;\n this.covenantThreshold = covenantThreshold;\n this.stakingTimeLock = stakingTimelock;\n this.unbondingTimeLock = unbondingTimelock;\n\n // Run the validate method to check if the provided script data is valid\n if (!this.validate()) {\n throw new Error(\"Invalid script data provided\");\n }\n }\n\n /**\n * Validates the staking script.\n * @returns {boolean} Returns true if the staking script is valid, otherwise false.\n */\n validate(): boolean {\n // check that staker key is the correct length\n if (this.stakerKey.length != NO_COORD_PK_BYTE_LENGTH) {\n return false;\n }\n // check that finalityProvider keys are the correct length\n if (\n this.finalityProviderKeys.some(\n (finalityProviderKey) =>\n finalityProviderKey.length != NO_COORD_PK_BYTE_LENGTH,\n )\n ) {\n return false;\n }\n // check that covenant keys are the correct length\n if (\n this.covenantKeys.some(\n (covenantKey) => covenantKey.length != NO_COOR