PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-staking/src/babylon
Просмотр файла: delegationMessage.ts
/**
* https://github.com/babylonlabs-io/babylon/blob/v1.99.0-snapshot.250211/x/btcstaking/types/validate_parsed_message.go
*/
import assert from 'assert';
import { BIP322 } from 'bip322-js';
import * as vendor from '@bitgo/babylonlabs-io-btc-staking-ts';
import * as babylonProtobuf from '@babylonlabs-io/babylon-proto-ts';
import * as bitcoinjslib from 'bitcoinjs-lib';
import * as utxolib from '@bitgo/utxo-lib';
import { Descriptor } from '@bitgo/wasm-miniscript';
import { toXOnlyPublicKey } from '@bitgo/utxo-core';
import { signWithKey, toWrappedPsbt } from '@bitgo/utxo-core/descriptor';
import { BabylonDescriptorBuilder } from './descriptor';
import { createStakingManager } from './stakingManager';
import { getStakingParams } from './stakingParams';
import { BabylonNetworkLike, toBitcoinJsNetwork } from './network';
export type ValueWithTypeUrl<T> = { typeUrl: string; value: T };
/**
* Decode a hex or base64 encoded string and check if the length is valid.
* @param v
* @param encoding
*/
function decodeCheck(v: string, encoding: 'hex' | 'base64') {
const result = Buffer.from(v, encoding);
if (result.toString(encoding).length !== v.length) {
throw new Error(`Invalid ${encoding} encoding`);
}
return result;
}
/**
* Convert a Buffer or string to a base64 encoded string.
* @param v
*/
function toBase64(v: Buffer | string) {
if (typeof v === 'string') {
for (const encoding of ['base64', 'hex'] as const) {
try {
return toBase64(decodeCheck(v, encoding));
} catch (e) {
// try next
}
}
throw new Error(`Invalid base64 or hex encoding: ${v}`);
}
return v.toString('base64');
}
export function getSignedPsbt(
psbt: bitcoinjslib.Psbt,
descriptor: Descriptor,
signers: utxolib.ECPairInterface[],
{ finalize = false }
): bitcoinjslib.Psbt {
const wrappedPsbt = toWrappedPsbt(psbt.toBuffer());
const signedInputs = psbt.data.inputs.flatMap((input, i) => {
assert(input.witnessUtxo);
if (Buffer.from(descriptor.scriptPubkey()).equals(input.witnessUtxo.script)) {
wrappedPsbt.updateInputWithDescriptor(i, descriptor);
const signResults = signers.map((signer) => {
assert(signer.privateKey);
return wrappedPsbt.signWithPrv(signer.privateKey);
});
return [[i, signResults]];
}
return [];
});
assert(signedInputs.length > 0);
if (finalize) {
wrappedPsbt.finalize();
}
return bitcoinjslib.Psbt.fromBuffer(Buffer.from(wrappedPsbt.serialize()));
}
/**
* Utility method to work around a bug in btc-staking-ts
* https://github.com/babylonlabs-io/btc-staking-ts/issues/71
* @param v
* @param network
*/
export function forceFinalizePsbt(
v: Buffer | utxolib.Psbt | bitcoinjslib.Psbt,
network: BabylonNetworkLike
): bitcoinjslib.Psbt {
if (v instanceof utxolib.Psbt) {
v = v.toBuffer();
}
if (v instanceof bitcoinjslib.Psbt) {
v = v.toBuffer();
}
const psbt = bitcoinjslib.Psbt.fromBuffer(v, { network: toBitcoinJsNetwork(network) });
// this only works with certain bitcoinjslib versions
psbt.finalizeAllInputs();
return psbt;
}
export function getBtcProviderForECKey(
descriptorBuilder: BabylonDescriptorBuilder,
stakerKey: utxolib.ECPairInterface
): vendor.BtcProvider {
function signWithDescriptor(
psbt: bitcoinjslib.Psbt,
descriptor: Descriptor,
key: utxolib.ECPairInterface
): bitcoinjslib.Psbt {
psbt = getSignedPsbt(psbt, descriptor, [key], { finalize: false });
// BUG: we need to blindly finalize here even though we have not fully signed
psbt.finalizeAllInputs();
return psbt;
}
function signBip322Simple(message: string): string {
// Get the script public key from the staking descriptor
const scriptPubKey = Buffer.from(descriptorBuilder.getStakingDescriptor().scriptPubkey());
const toSpendTx = BIP322.buildToSpendTx(message, scriptPubKey);
// Get the to_spend txid
const toSpendTxId = toSpendTx.getId();
// Create PSBT object for constructing the transaction
const toSignPsbt = new bitcoinjslib.Psbt();
toSignPsbt.setVersion(2); // nVersion = 0
toSignPsbt.setLocktime(0); // nLockTime = 0
toSignPsbt.addInput({
hash: toSpendTxId,
index: 0,
sequence: descriptorBuilder.stakingTimeLock,
witnessUtxo: {
script: scriptPubKey,
value: 0,
},
});
// Sign the PSBT with the staker key
const wrappedPsbt = toWrappedPsbt(toSignPsbt.toBuffer());
wrappedPsbt.updateInputWithDescriptor(0, descriptorBuilder.getStakingDescriptor());
signWithKey(wrappedPsbt, stakerKey);
wrappedPsbt.finalize();
// Encode the witness data and return
return BIP322.encodeWitness(bitcoinjslib.Psbt.fromBuffer(Buffer.from(wrappedPsbt.serialize())));
}
return {
/**
* @param signingStep
* @param message
* @param type
* @returns Base64 encoded string
*/
async signMessage(
signingStep: vendor.SigningStep,
message: string,
type: 'ecdsa' | 'bip322-simple'
): Promise<string> {
assert(signingStep === 'proof-of-possession');
switch (type) {
case 'ecdsa':
return toBase64(stakerKey.sign(Buffer.from(message, 'hex')));
case 'bip322-simple':
return toBase64(signBip322Simple(message));
default:
throw new Error(`unexpected signing step: ${signingStep}`);
}
},
async signPsbt(signingStep: vendor.SigningStep, psbtHex: string): Promise<string> {
const psbt = bitcoinjslib.Psbt.fromHex(psbtHex);
switch (signingStep) {
case 'staking-slashing':
return signWithDescriptor(psbt, descriptorBuilder.getStakingDescriptor(), stakerKey).toHex();
case 'unbonding-slashing':
return signWithDescriptor(psbt, descriptorBuilder.getUnbondingDescriptor(), stakerKey).toHex();
default:
throw new Error(`unexpected signing step: ${signingStep}`);
}
},
};
}
type Result = {
unsignedDelegationMsg: ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>;
stakingTx: bitcoinjslib.Transaction;
};
/**
* @param stakingKey - this is the single-sig key that is used for co-signing the staking output
* @param changeAddress - this is unrelated to the staking key and is used for the change output
*/
export function toStakerInfo(
stakingKey: utxolib.ECPairInterface | Buffer | string,
changeAddress: string
): vendor.StakerInfo {
if (typeof stakingKey === 'object' && 'publicKey' in stakingKey) {
stakingKey = stakingKey.publicKey;
}
if (typeof stakingKey === 'string') {
stakingKey = Buffer.from(stakingKey, 'hex');
}
return {
publicKeyNoCoordHex: toXOnlyPublicKey(stakingKey).toString('hex'),
address: changeAddress,
};
}
export function createStaking(
network: BabylonNetworkLike,
blockHeight: number,
stakerBtcInfo: vendor.StakerInfo,
stakingInput: vendor.StakingInputs,
versionedParams: vendor.VersionedStakingParams[] = getStakingParams(network)
): vendor.Staking {
if (blockHeight === 0) {
throw new Error('Babylon BTC tip height cannot be 0');
}
// Get the Babylon params based on the BTC tip height from Babylon chain
const params = vendor.getBabylonParamByBtcHeight(blockHeight, versionedParams);
return new vendor.Staking(
toBitcoinJsNetwork(network),
stakerBtcInfo,
params,
stakingInput.finalityProviderPkNoCoordHex,
stakingInput.stakingTimelock
);
}
type TransactionLike =
| bitcoinjslib.Psbt
| bitcoinjslib.Transaction
| utxolib.Transaction
| utxolib.bitgo.UtxoTransaction<bigint | number>
| utxolib.Psbt
| utxolib.bitgo.UtxoPsbt;
function toStakingTransactionFromPsbt(
psbt: bitcoinjslib.Psbt | utxolib.Psbt | utxolib.bitgo.UtxoPsbt
): bitcoinjslib.Transaction {
if (!(psbt instanceof utxolib.bitgo.UtxoPsbt)) {
psbt = utxolib.bitgo.createPsbtFromBuffer(psbt.toBuffer(), utxolib.networks.bitcoin);
}
if (psbt instanceof utxolib.bitgo.UtxoPsbt) {
// only utxolib.bitgo.UtxoPsbt has the getUnsignedTx method
return bitcoinjslib.Transaction.fromHex(psbt.getUnsignedTx().toHex());
}
throw new Error('illegal state');
}
export function toStakingTransaction(tx: TransactionLike): bitcoinjslib.Transaction {
if (tx instanceof bitcoinjslib.Psbt || tx instanceof utxolib.Psbt) {
return toStakingTransactionFromPsbt(tx);
}
return bitcoinjslib.Transaction.fromHex(tx.toHex());
}
/*
* 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
*
* The difference is that here we are returning an _unsigned_ delegation message.
*/
export async function createDelegationMessageWithTransaction(
manager: vendor.BabylonBtcStakingManager,
staking: vendor.Staking,
stakingAmountSat: number,
transaction: TransactionLike,
babylonAddress: string
): Promise<ValueWithTypeUrl<babylonProtobuf.btcstakingtx.MsgCreateBTCDelegation>> {
if (!vendor.isValidBabylonAddress(babylonAddress)) {
throw new Error('Invalid Babylon address');
}
// Create delegation message without including inclusion proof
return manager.createBtcDelegationMsg(
staking,
{
stakingTimelock: staking.stakingTimelock,
finalityProviderPkNoCoordHex: staking.finalityProviderPkNoCoordHex,
stakingAmountSat,
},
toStakingTransaction(transaction),
babylonAddress,
staking.stakerInfo,
staking.params
);
}
export async function createUnsignedPreStakeRegistrationBabylonTransactionWithBtcProvider(
btcProvider: vendor.BtcProvider,
network: bitcoinjslib.Network,
stakerBtcInfo: vendor.StakerInfo,
stakingInput: vendor.StakingInputs,
babylonBtcTipHeight: number,
inputUTXOs: vendor.UTXO[],
feeRateSatB: number,
babylonAddress: string,
stakingParams: vendor.VersionedStakingParams[] = getStakingParams(network)
): Promise<Result> {
if (inputUTXOs.length === 0) {
throw new Error('No input UTXOs provided');
}
const manager = createStakingManager(network, btcProvider, stakingParams);
const staking = createStaking(network, babylonBtcTipHeight, stakerBtcInfo, stakingInput, stakingParams);
// Create unsigned staking transaction
const { transaction } = staking.createStakingTransaction(stakingInput.stakingAmountSat, inputUTXOs, feeRateSatB);
const unsignedDelegationMsg = await createDelegationMessageWithTransaction(
manager,
staking,
stakingInput.stakingAmountSat,
transaction,
babylonAddress
);
return { unsignedDelegationMsg, stakingTx: transaction };
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!