Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/abstract-utxo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"@bitgo/utxo-core": "^1.40.0",
"@bitgo/utxo-descriptors": "^1.4.0",
"@bitgo/utxo-ord": "^1.33.0",
"@bitgo/wasm-utxo": "^4.27.0",
"@bitgo/wasm-utxo": "^4.39.0",
"@types/lodash": "^4.14.121",
"@types/superagent": "4.1.15",
"bignumber.js": "^9.0.2",
Expand Down
4 changes: 2 additions & 2 deletions modules/sdk-coin-stx/src/lib/abstractContractBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export abstract class AbstractContractBuilder extends TransactionBuilder {
postConditions: this._postConditions,
anchorMode: this._anchorMode,
network: this._network,
fee: new BigNum(this._fee.fee),
nonce: new BigNum(this._nonce),
fee: new BigNum(this._fee.fee).toString(),
nonce: new BigNum(this._nonce).toString(),
};
if (this._fromPubKeys.length === 1) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export class FungibleTokenTransferBuilder extends AbstractContractBuilder {
this._numberSignatures
).address,
FungibleConditionCode.Equal,
amount,
amount.toString(),
`${this._contractAddress}.${this._contractName}::${this._tokenName}`
),
];
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-stx/src/lib/sbtcWithdrawBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class SbtcWithdrawBuilder extends AbstractContractBuilder {
this._numberSignatures
).address,
FungibleConditionCode.Equal,
amount,
amount.toString(),
createAssetInfo(sbtcContractAddress, SBTC_TOKEN_CONTRACT_NAME, SBTC_TOKEN_ASSET_NAME)
),
];
Expand Down
2 changes: 1 addition & 1 deletion modules/sdk-coin-stx/src/lib/sendmanyBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class SendmanyBuilder extends AbstractContractBuilder {
this._numberSignatures
).address,
FungibleConditionCode.Equal,
sum
sum.toString()
),
];
}
Expand Down
15 changes: 10 additions & 5 deletions modules/sdk-coin-stx/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
this._numberSignatures = 1;
if (tx.stxTransaction.auth.spendingCondition.signature.data !== emptyMessageSignature().data) {
const signature = tx.stxTransaction.auth.spendingCondition.signature;
sigHash = makeSigHashPreSign(sigHash, authType, new BigNum(this._fee.fee), new BigNum(this._nonce));
sigHash = makeSigHashPreSign(
sigHash,
authType,
new BigNum(this._fee.fee).toString(),
new BigNum(this._nonce).toString()
);
this._signatures.push({ ...signature, index: 0, sigHash });
this._fromPubKeys = [publicKeyFromSignature(sigHash, signature)];
}
Expand All @@ -88,8 +93,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
const nextVerify = nextVerification(
sigHash,
authType,
new BigNum(this._fee.fee),
new BigNum(this._nonce),
new BigNum(this._fee.fee).toString(),
new BigNum(this._nonce).toString(),
PubKeyEncoding.Compressed, // useless param as Compressed is hardcoded in stacks lib
signature
);
Expand Down Expand Up @@ -119,8 +124,8 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
const isMultiSig: boolean = this._fromPubKeys.length > 1;
this._transaction.stxTransaction.setFee(new BigNum(this._fee.fee));
this._transaction.stxTransaction.setNonce(new BigNum(this._nonce));
this._transaction.stxTransaction.setFee(new BigNum(this._fee.fee).toString());
this._transaction.stxTransaction.setNonce(new BigNum(this._nonce).toString());

for (let index = 0; index < this._fromPubKeys.length; index++) {
const pubKey = this._fromPubKeys[index];
Expand Down
6 changes: 3 additions & 3 deletions modules/sdk-coin-stx/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ export class TransferBuilder extends TransactionBuilder {
private buildTokenTransferOptions(): UnsignedTokenTransferOptions | UnsignedMultiSigTokenTransferOptions {
const defaultOpts: TokenTransferOptions = {
recipient: this._toAddress,
amount: this._amount,
amount: this._amount.toString(),
memo: this._memo,
anchorMode: this._anchorMode,
network: this._network,
fee: new BigNum(this._fee.fee),
nonce: new BigNum(this._nonce),
fee: new BigNum(this._fee.fee).toString(),
nonce: new BigNum(this._nonce).toString(),
};
if (this._fromPubKeys.length > 0) {
if (this._fromPubKeys.length === 1) {
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-bin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@bitgo/unspents": "^0.51.8",
"@bitgo/utxo-core": "^1.40.0",
"@bitgo/utxo-lib": "^11.24.2",
"@bitgo/wasm-utxo": "^4.27.0",
"@bitgo/wasm-utxo": "^4.39.0",
"@noble/curves": "1.8.1",
"archy": "^1.0.0",
"bech32": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion modules/utxo-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"@bitgo/secp256k1": "^1.11.0",
"@bitgo/unspents": "^0.51.8",
"@bitgo/utxo-lib": "^11.24.2",
"@bitgo/wasm-utxo": "^4.27.0",
"@bitgo/wasm-utxo": "^4.39.0",
"bip174": "npm:@bitgo-forks/bip174@3.1.0-master.4",
"fast-sha256": "^1.3.0"
},
Expand Down
5 changes: 4 additions & 1 deletion modules/utxo-descriptors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
},
"dependencies": {
"@bitgo/utxo-core": "^1.40.0",
"@bitgo/wasm-utxo": "^4.27.0"
"@bitgo/wasm-utxo": "^4.39.0"
},
"devDependencies": {
"@stacks/bitcoin-staking": "7.6.0"
}
}
1 change: 1 addition & 0 deletions modules/utxo-descriptors/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * as sbtc from './sbtc';
export * as pox5 from './pox5';
100 changes: 100 additions & 0 deletions modules/utxo-descriptors/src/pox5/descriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ast, bip32, Descriptor } from '@bitgo/wasm-utxo';

type BIP32Interface = bip32.BIP32Interface;

export type Pox5StakerKey = BIP32Interface | Buffer;

export type Pox5LockupDescriptorParams = {
unlockHeight: number;
/** sha256(sha256(Clarity consensus principal bytes)). */
stakerCommitment: Buffer;
earlyExitKey: Buffer;
/** User, backup, and BitGo keys, all BIP32 or all concrete compressed keys. */
stakerKeys: [Pox5StakerKey, Pox5StakerKey, Pox5StakerKey];
};

function validateCompressedKey(key: Buffer, field: string): void {
if (key.length !== 33 || (key[0] !== 0x02 && key[0] !== 0x03)) {
throw new Error(`${field} must be a 33-byte compressed public key`);
}
}

function isBip32Triple(
keys: Pox5LockupDescriptorParams['stakerKeys']
): keys is [BIP32Interface, BIP32Interface, BIP32Interface] {
return keys.every((key) => !Buffer.isBuffer(key));
}

function asDescriptorKey(key: Pox5StakerKey, field: string): string {
if (Buffer.isBuffer(key)) {
validateCompressedKey(key, field);
return key.toString('hex');
}
return key.neutered().toBase58() + '/*';
}

function validateParams(params: Pox5LockupDescriptorParams): void {
if (!Number.isSafeInteger(params.unlockHeight) || params.unlockHeight <= 0 || params.unlockHeight >= 500_000_000) {
throw new Error(`unlockHeight (${params.unlockHeight}) must be a positive block height below 500000000`);
}
if (params.stakerCommitment.length !== 32) {
throw new Error(`stakerCommitment must be 32 bytes (got ${params.stakerCommitment.length})`);
}
validateCompressedKey(params.earlyExitKey, 'earlyExitKey');

const bufferCount = params.stakerKeys.filter(Buffer.isBuffer).length;
if (bufferCount !== 0 && bufferCount !== params.stakerKeys.length) {
throw new Error('stakerKeys must contain either three BIP32 keys or three compressed public keys');
}
params.stakerKeys.forEach((key, index) => {
if (Buffer.isBuffer(key)) {
validateCompressedKey(key, `stakerKeys[${index}]`);
}
});
}

/**
* Build the canonical PoX-5 P2WSH descriptor. The post-CLTV and early-exit
* paths share BitGo's standard 2-of-3 compressed-key multisig tail.
*/
export function createPox5LockupDescriptor(params: Pox5LockupDescriptorParams): string {
validateParams(params);
const stakerKeys = params.stakerKeys.map((key, index) => asDescriptorKey(key, `stakerKeys[${index}]`));
const miniscript: ast.MiniscriptNode = {
and_v: [
{
'v:or_i': [
{ after: params.unlockHeight },
{
and_v: [
{ 'v:sha256': params.stakerCommitment.toString('hex') },
{ pk: params.earlyExitKey.toString('hex') },
],
},
],
},
{ multi: [2, ...stakerKeys] },
],
};
return ast.formatNode({ wsh: miniscript });
}

/** Compile the PoX-5 P2WSH scriptPubKey at a BIP32 derivation index. */
export function createPox5LockupScriptPubKey(params: Pox5LockupDescriptorParams, derivationIndex = 0): Buffer {
const descriptor = createPox5LockupDescriptor(params);
if (isBip32Triple(params.stakerKeys)) {
return Buffer.from(
Descriptor.fromString(descriptor, 'derivable').atDerivationIndex(derivationIndex).scriptPubkey()
);
}
return Buffer.from(Descriptor.fromString(descriptor, 'definite').scriptPubkey());
}

/** Derive the compressed staker keys needed to prepare a witness at an index. */
export function derivePox5StakerKeys(
stakerKeys: [BIP32Interface, BIP32Interface, BIP32Interface],
index: number
): [Buffer, Buffer, Buffer] {
const keys = stakerKeys.map((key) => Buffer.from(key.derive(index).publicKey));
return [keys[0], keys[1], keys[2]];
}
2 changes: 2 additions & 0 deletions modules/utxo-descriptors/src/pox5/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './descriptor';
export * from './parseDescriptor';
112 changes: 112 additions & 0 deletions modules/utxo-descriptors/src/pox5/parseDescriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { BIP32, Descriptor, ast } from '@bitgo/wasm-utxo';
import { Pattern, PatternMatcher } from '@bitgo/utxo-core/descriptor';

export type ParsedPox5LockupDescriptor = {
unlockHeight: number;
stakerCommitment: Buffer;
earlyExitKey: Buffer;
stakerKeyStrings: [string, string, string];
stakerKeys: [Buffer, Buffer, Buffer] | undefined;
miniscriptNode: ast.MiniscriptNode;
};

const COMPRESSED_KEY = /^(02|03)[0-9a-fA-F]{64}$/;
const XPUB_WITH_INDEX = /^([1-9A-HJ-NP-Za-km-z]+)\/(\d+)$/;

function asNumber(value: unknown, field: string): number {
if (typeof value !== 'number') {
throw new Error(`${field} must be a number`);
}
return value;
}

function asString(value: unknown, field: string): string {
if (typeof value !== 'string') {
throw new Error(`${field} must be a string`);
}
return value;
}

function parseCompressedKey(value: string, field: string): Buffer {
if (!COMPRESSED_KEY.test(value)) {
throw new Error(`${field} must be a compressed public key`);
}
return Buffer.from(value, 'hex');
}

function resolveStakerKey(value: string): Buffer | undefined {
if (COMPRESSED_KEY.test(value)) {
return Buffer.from(value, 'hex');
}
const match = value.match(XPUB_WITH_INDEX);
if (!match) {
return undefined;
}
const [, xpub, indexString] = match;
const index = Number.parseInt(indexString, 10);
try {
return Buffer.from(BIP32.fromBase58(xpub).derive(index).publicKey);
} catch {
return undefined;
}
}

/**
* Parse only the canonical PoX-5 descriptor template. Other descriptors return
* null; malformed fields within the template throw so callers cannot finalize
* a script under an ambiguous policy.
*/
export function parsePox5LockupDescriptor(
descriptor: Descriptor | ast.DescriptorNode
): ParsedPox5LockupDescriptor | null {
const matcher = new PatternMatcher();
const descriptorNode = descriptor instanceof Descriptor ? ast.fromDescriptor(descriptor) : descriptor;
const matched = matcher.match(descriptorNode, { wsh: { $var: 'miniscript' } });
if (!matched) {
return null;
}

const miniscriptNode = matched.miniscript as ast.MiniscriptNode;
const pattern: Pattern = {
and_v: [
{
'v:or_i': [
{ after: { $var: 'unlockHeight' } },
{ and_v: [{ 'v:sha256': { $var: 'stakerCommitment' } }, { pk: { $var: 'earlyExitKey' } }] },
],
},
{ multi: { $var: 'stakerMulti' } },
],
};
const fields = matcher.match(miniscriptNode, pattern);
if (!fields) {
return null;
}

const unlockHeight = asNumber(fields.unlockHeight, 'after argument');
if (!Number.isSafeInteger(unlockHeight) || unlockHeight <= 0 || unlockHeight >= 500_000_000) {
throw new Error(`unlockHeight (${unlockHeight}) must be a positive block height below 500000000`);
}
const commitmentHex = asString(fields.stakerCommitment, 'sha256 commitment');
if (!/^[0-9a-fA-F]{64}$/.test(commitmentHex)) {
throw new Error('stakerCommitment must be 32 bytes');
}
const earlyExitKey = parseCompressedKey(asString(fields.earlyExitKey, 'early exit key'), 'earlyExitKey');

if (!Array.isArray(fields.stakerMulti) || fields.stakerMulti.length !== 4 || fields.stakerMulti[0] !== 2) {
throw new Error('staker multi must be a 2-of-3 multisig');
}
const stakerKeyStrings = fields.stakerMulti.slice(1).map((key, index) => asString(key, `staker key ${index}`));
const resolvedKeys = stakerKeyStrings.map(resolveStakerKey);

return {
unlockHeight,
stakerCommitment: Buffer.from(commitmentHex, 'hex'),
earlyExitKey,
stakerKeyStrings: [stakerKeyStrings[0], stakerKeyStrings[1], stakerKeyStrings[2]],
stakerKeys: resolvedKeys.every((key): key is Buffer => key !== undefined)
? [resolvedKeys[0], resolvedKeys[1], resolvedKeys[2]]
: undefined,
miniscriptNode,
};
}
Loading
Loading