PHP WebShell
Текущая директория: /opt/BitGoJS/modules/utxo-staking/test/unit/babylon
Просмотр файла: transactions.ts
import assert from 'assert';
import * as vendor from '@bitgo/babylonlabs-io-btc-staking-ts';
import * as bitcoinjslib from 'bitcoinjs-lib';
import * as utxolib from '@bitgo/utxo-lib';
import { ECPairInterface } from '@bitgo/utxo-lib';
import { ast, Descriptor, Miniscript } from '@bitgo/wasm-miniscript';
import {
createAddressFromDescriptor,
createPsbt,
getNewSignatureCount,
signWithKey,
toUtxoPsbt,
toWrappedPsbt,
} from '@bitgo/utxo-core/descriptor';
import { getFixture, toPlainObject } from '@bitgo/utxo-core/testutil';
import { getBabylonParamByVersion } from '@bitgo/babylonlabs-io-btc-staking-ts';
import {
BabylonDescriptorBuilder,
testnetFinalityProvider0,
getSignedPsbt,
getStakingParams,
toStakerInfo,
forceFinalizePsbt,
} from '../../../src/babylon';
import { normalize } from '../fixtures.utils';
import { fromXOnlyPublicKey, getECKey, getECKeys, getXOnlyPubkey } from './key.utils';
import { getBitGoUtxoStakingMsgCreateBtcDelegation, getVendorMsgCreateBtcDelegation } from './vendor.utils';
type WithFee<T> = T & { fee: number };
type TransactionWithFee = WithFee<{ transaction: bitcoinjslib.Transaction }>;
type PsbtWithFee = WithFee<{ psbt: bitcoinjslib.Psbt }>;
type TransactionTree = {
staking: TransactionWithFee;
stakingWithdraw: PsbtWithFee;
unbonding: TransactionWithFee;
unbondingWithdraw: PsbtWithFee;
unbondingSlashing: PsbtWithFee;
unbondingSlashingWithdraw: PsbtWithFee | undefined;
slashing: PsbtWithFee;
slashingWithdraw: PsbtWithFee | undefined;
slashingSigned: bitcoinjslib.Psbt | undefined;
slashingSignedBase64: string | undefined;
};
function getStakingTransactionTreeVendor(
builder: vendor.Staking,
amount: number,
utxos: vendor.UTXO[],
feeRateSatB: number,
signers:
| {
staker: ECPairInterface;
finalityProvider: ECPairInterface;
covenant: ECPairInterface[];
covenantThreshold: number;
}
| { staker: ECPairInterface },
descriptorBuilder: BabylonDescriptorBuilder
): TransactionTree {
const staking = builder.createStakingTransaction(amount, utxos, feeRateSatB);
const stakingWithdraw = builder.createWithdrawStakingExpiredPsbt(staking.transaction, feeRateSatB);
const unbonding = builder.createUnbondingTransaction(staking.transaction);
const unbondingWithdraw = builder.createWithdrawEarlyUnbondedTransaction(unbonding.transaction, feeRateSatB);
const unbondingSlashing = builder.createUnbondingOutputSlashingPsbt(unbonding.transaction);
const signSequence = [signers.staker];
if ('finalityProvider' in signers) {
signSequence.push(signers.finalityProvider, ...signers.covenant);
}
const unbondingSlashingWithdraw = signSequence
? builder.createWithdrawSlashingPsbt(
forceFinalizePsbt(
getSignedPsbt(unbondingSlashing.psbt, descriptorBuilder.getUnbondingDescriptor(), signSequence, {
finalize: false,
}),
builder.network
).extractTransaction(),
feeRateSatB
)
: undefined;
const slashing = builder.createStakingOutputSlashingPsbt(staking.transaction);
const slashingSigned = signSequence
? getSignedPsbt(slashing.psbt, descriptorBuilder.getStakingDescriptor(), signSequence, {
finalize: false,
})
: undefined;
const slashingWithdraw = slashingSigned
? builder.createWithdrawSlashingPsbt(
forceFinalizePsbt(slashingSigned.toBuffer(), builder.network).extractTransaction(),
feeRateSatB
)
: undefined;
return {
staking,
stakingWithdraw,
unbonding,
unbondingWithdraw,
unbondingSlashing,
unbondingSlashingWithdraw,
slashing,
slashingSigned,
slashingSignedBase64: slashingSigned?.toBuffer().toString('base64'),
slashingWithdraw,
};
}
function createUnstakingTransaction(
stakingTx: vendor.TransactionResult,
stakingDescriptor: Descriptor,
changeAddress: string,
{ sequence }: { sequence: number }
): utxolib.Psbt {
const network = utxolib.networks.bitcoin;
const witnessUtxoNumber = stakingTx.transaction.outs[0];
const witnessUtxo = {
script: witnessUtxoNumber.script,
value: BigInt(witnessUtxoNumber.value),
};
return createPsbt(
{
network,
},
[
{
hash: stakingTx.transaction.getId(),
index: 0,
witnessUtxo,
descriptor: stakingDescriptor,
sequence,
},
],
[
{
script: utxolib.address.toOutputScript(changeAddress, network),
value: BigInt(witnessUtxoNumber.value) - 1000n,
},
]
);
}
function getTestnetStakingParamsWithCovenant(
params: vendor.StakingParams,
covenantKeys: ECPairInterface[]
): vendor.StakingParams {
return {
...params,
covenantNoCoordPks: covenantKeys.map((pk) => getXOnlyPubkey(pk).toString('hex')),
};
}
function wpkhDescriptor(key: utxolib.ECPairInterface): Descriptor {
return Descriptor.fromString(ast.formatNode({ wpkh: key.publicKey.toString('hex') }), 'definite');
}
function mockUtxo(descriptor: Descriptor): vendor.UTXO {
const scriptPubKey = Buffer.from(descriptor.scriptPubkey());
const witnessScript = Buffer.from(descriptor.encode());
return {
rawTxHex: undefined,
txid: Buffer.alloc(32).fill(0x11).toString('hex'),
value: 666_666,
vout: 0,
redeemScript: undefined,
witnessScript: witnessScript.toString('hex'),
scriptPubKey: scriptPubKey.toString('hex'),
};
}
function parseScript(key: string, script: unknown) {
if (!Buffer.isBuffer(script)) {
throw new Error('script must be a buffer');
}
const ms = Miniscript.fromBitcoinScript(script, 'tap');
return {
script: script.toString('hex'),
miniscript: ms.toString(),
miniscriptAst: ast.fromMiniscript(ms),
scriptASM: utxolib.script.toASM(script).split(/\s+/),
};
}
function parseScripts(scripts: unknown) {
if (typeof scripts !== 'object' || scripts === null) {
throw new Error('scripts must be an object');
}
return Object.fromEntries(Object.entries(scripts).map(([key, value]) => [key, parseScript(key, value)]));
}
type EqualsAssertion = typeof assert.deepStrictEqual;
async function assertEqualsFixture(
fixtureName: string,
value: unknown,
n = normalize,
eq: EqualsAssertion = assert.deepStrictEqual
): Promise<void> {
value = n(value);
eq(await getFixture(fixtureName, value), value);
}
async function assertScriptsEqualFixture(
fixtureName: string,
builder: vendor.StakingScriptData,
scripts: unknown
): Promise<void> {
await assertEqualsFixture(fixtureName, {
builder: toPlainObject(builder),
scripts: parseScripts(scripts),
});
}
async function assertTransactionEqualsFixture(fixtureName: string, tx: unknown): Promise<void> {
await assertEqualsFixture(fixtureName, normalize(tx));
}
function assertEqualsMiniscript(script: Buffer, miniscript: ast.MiniscriptNode): void {
const ms = Miniscript.fromBitcoinScript(script, 'tap');
assert.deepStrictEqual(ast.fromMiniscript(ms), miniscript);
assert.deepStrictEqual(
script.toString('hex'),
Buffer.from(Miniscript.fromString(ast.formatNode(miniscript), 'tap').encode()).toString('hex')
);
}
function assertEqualScripts(descriptorBuilder: BabylonDescriptorBuilder, builder: vendor.StakingScripts) {
for (const [key, script] of Object.entries(builder) as [keyof vendor.StakingScripts, Buffer][]) {
switch (key) {
case 'timelockScript':
assertEqualsMiniscript(script, descriptorBuilder.getTimelockMiniscript());
break;
case 'unbondingScript':
assertEqualsMiniscript(script, descriptorBuilder.getUnbondingMiniscript());
break;
case 'slashingScript':
assertEqualsMiniscript(script, descriptorBuilder.getSlashingMiniscript());
break;
case 'unbondingTimelockScript':
assertEqualsMiniscript(script, descriptorBuilder.getUnbondingTimelockMiniscript());
break;
default:
throw new Error(`unexpected script key: ${key}`);
}
}
}
function assertEqualOutputScript(outputInfo: { scriptPubKey: Buffer }, descriptor: Descriptor) {
assert.strictEqual(outputInfo.scriptPubKey.toString('hex'), Buffer.from(descriptor.scriptPubkey()).toString('hex'));
}
function describeWithKeys(
tag: string,
finalityProviderKeys: ECPairInterface[],
covenantKeys: ECPairInterface[],
stakingParams: vendor.StakingParams,
{ signIntermediateTxs = false } = {}
) {
const stakerKey = getECKey('staker') as ECPairInterface & { privateKey: Buffer };
const covenantThreshold = stakingParams.covenantQuorum;
const stakingTimelock = stakingParams.minStakingTimeBlocks;
const unbondingTimelock = stakingParams.unbondingTime;
const vendorBuilder = new vendor.StakingScriptData(
getXOnlyPubkey(stakerKey),
finalityProviderKeys.map(getXOnlyPubkey),
covenantKeys.map(getXOnlyPubkey),
covenantThreshold,
stakingTimelock,
unbondingTimelock
);
const descriptorBuilder = new BabylonDescriptorBuilder(
getXOnlyPubkey(stakerKey),
finalityProviderKeys.map(getXOnlyPubkey),
covenantKeys.map(getXOnlyPubkey),
covenantThreshold,
stakingTimelock,
unbondingTimelock
);
describe(`Babylon Staking [${tag}]`, function () {
it('generates expected staking scripts', async function () {
await assertScriptsEqualFixture(
`test/fixtures/babylon/scripts.${tag}.json`,
vendorBuilder,
vendorBuilder.buildScripts()
);
});
it('matches inner taproot scripts', function () {
assertEqualScripts(descriptorBuilder, vendorBuilder.buildScripts());
});
it('matches output scripts', function () {
assertEqualOutputScript(
vendor.deriveStakingOutputInfo(vendorBuilder.buildScripts(), bitcoinjslib.networks.bitcoin),
descriptorBuilder.getStakingDescriptor()
);
assertEqualOutputScript(
vendor.deriveSlashingOutput(vendorBuilder.buildScripts(), bitcoinjslib.networks.bitcoin),
descriptorBuilder.getSlashingDescriptor()
);
assertEqualOutputScript(
vendor.deriveUnbondingOutputInfo(vendorBuilder.buildScripts(), bitcoinjslib.networks.bitcoin),
descriptorBuilder.getUnbondingDescriptor()
);
});
describe('Transaction Sets', async function () {
const stakerMainWalletKey = getECKey('stakerMainWallet');
const mainWallet = wpkhDescriptor(stakerMainWalletKey);
const amount = 55_555;
const changeAddress = createAddressFromDescriptor(mainWallet, undefined, utxolib.networks.bitcoin);
const feeRateSatB = 2;
const utxo = mockUtxo(mainWallet);
let stakingTx: vendor.TransactionResult;
before('setup stakingTx', function () {
stakingTx = vendor.stakingTransaction(
vendorBuilder.buildScripts(),
amount,
changeAddress,
[mockUtxo(mainWallet)],
bitcoinjslib.networks.bitcoin,
feeRateSatB
);
});
it('has expected transactions', async function () {
await assertTransactionEqualsFixture(`test/fixtures/babylon/stakingTransaction.${tag}.json`, stakingTx);
// simply one staking output and one change output
// nothing special
assert.deepStrictEqual(stakingTx.transaction.outs, [
{
script: Buffer.from(descriptorBuilder.getStakingDescriptor().scriptPubkey()),
value: amount,
},
{
script: utxolib.address.toOutputScript(changeAddress, utxolib.networks.bitcoin),
value: utxo.value - amount - stakingTx.fee,
},
]);
});
if (finalityProviderKeys.length !== 1) {
return;
}
const finalityProvider = finalityProviderKeys[0];
it('has expected transactions (vendorStaking.Staking)', async function (this: Mocha.Context) {
const vendorStakingTxBuilder = new vendor.Staking(
bitcoinjslib.networks.bitcoin,
toStakerInfo(stakerKey, changeAddress),
stakingParams,
getXOnlyPubkey(finalityProvider).toString('hex'),
stakingParams.minStakingTimeBlocks
);
const txTree = getStakingTransactionTreeVendor(
vendorStakingTxBuilder,
amount,
[utxo],
feeRateSatB,
signIntermediateTxs
? {
staker: stakerKey,
finalityProvider,
covenant: covenantKeys,
covenantThreshold: covenantThreshold,
}
: { staker: stakerKey },
descriptorBuilder
);
await assertTransactionEqualsFixture(`test/fixtures/babylon/txTree.${tag}.json`, txTree);
});
it('creates MsgCreateBTCDelegation', async function () {
type F = typeof getVendorMsgCreateBtcDelegation;
const fVendor: F = getVendorMsgCreateBtcDelegation;
const fBitGo: F = getBitGoUtxoStakingMsgCreateBtcDelegation;
for (const f of [fVendor, fBitGo]) {
await assertEqualsFixture(
`test/fixtures/babylon/msgCreateBTCDelegation.${tag}.json`,
await f(
bitcoinjslib.networks.bitcoin,
stakerKey,
finalityProvider,
descriptorBuilder,
[{ ...stakingParams, version: 0, btcActivationHeight: 0 }],
changeAddress,
amount,
utxo,
feeRateSatB,
800_000
),
normalize,
(a, b) => {
// The vendor library serializes the signature as BIP322, while
// our implementation serializes it as ECDSA.
// Strip the pop field from the MsgCreateBTCDelegation.
function stripPop(v: unknown) {
const vAny = v as any;
delete vAny['unsignedDelegationMsg']['value']['pop'];
}
stripPop(a);
stripPop(b);
assert.deepStrictEqual(a, b);
}
);
}
});
it('creates unstaking transaction', async function () {
const unstaking = createUnstakingTransaction(
stakingTx,
descriptorBuilder.getStakingDescriptor(),
changeAddress,
{ sequence: stakingParams.minStakingTimeBlocks }
);
const wrappedPsbt = toWrappedPsbt(unstaking);
assert(getNewSignatureCount(signWithKey(wrappedPsbt, stakerKey)) > 0);
wrappedPsbt.finalize();
const tx = toUtxoPsbt(wrappedPsbt, utxolib.networks.bitcoin).extractTransaction();
await assertTransactionEqualsFixture(`test/fixtures/babylon/unstakingTransaction.${tag}.json`, {
transaction: tx,
});
});
});
});
}
function describeWithKeysFromStakingParams(
tag: string,
finalityProviderKeys: ECPairInterface[],
stakingParams: vendor.StakingParams
) {
describeWithKeys(
tag,
finalityProviderKeys,
stakingParams.covenantNoCoordPks.map((pk) => fromXOnlyPublicKey(Buffer.from(pk, 'hex'))),
stakingParams
);
}
function describeWithMockKeys(
tag: string,
stakingParams: vendor.StakingParams,
finalityProviderKeys: ECPairInterface[],
covenantKeys: ECPairInterface[]
) {
describeWithKeys(
tag,
finalityProviderKeys,
covenantKeys,
getTestnetStakingParamsWithCovenant(stakingParams, covenantKeys),
{
signIntermediateTxs: true,
}
);
}
describeWithKeysFromStakingParams(
'testnet',
[fromXOnlyPublicKey(testnetFinalityProvider0)],
getBabylonParamByVersion(5, getStakingParams('testnet'))
);
describeWithMockKeys(
'testnetMock',
getBabylonParamByVersion(5, getStakingParams('testnet')),
getECKeys('finalityProvider', 1),
getECKeys('covenant', 9)
);
Выполнить команду
Для локальной разработки. Не используйте в интернете!