Skip to content

Commit 9e9a464

Browse files
authored
fix: pox4 stack-stx burn-op handling (#1936)
* fix: attempt at creating a pox4 stack-stx burn-op * feat: stack-stx burn-op tx succeeds, parsing TBD * fix: simplify pox-4 stack-stx burn-op parsing * chore: fix lint
1 parent b0e5720 commit 9e9a464

File tree

3 files changed

+187
-20
lines changed

3 files changed

+187
-20
lines changed

src/event-stream/core-node-message.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ interface FtBurnEvent extends CoreNodeEventBase {
148148
};
149149
}
150150

151-
interface BurnchainOpRegisterAssetNft {
151+
export interface BurnchainOpRegisterAssetNft {
152152
register_asset: {
153153
asset_type: 'nft';
154154
burn_header_hash: string;
@@ -158,7 +158,7 @@ interface BurnchainOpRegisterAssetNft {
158158
};
159159
}
160160

161-
interface BurnchainOpRegisterAssetFt {
161+
export interface BurnchainOpRegisterAssetFt {
162162
register_asset: {
163163
asset_type: 'ft';
164164
burn_header_hash: string;
@@ -168,7 +168,27 @@ interface BurnchainOpRegisterAssetFt {
168168
};
169169
}
170170

171-
export type BurnchainOp = BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt;
171+
export interface BurnchainOpStackStx {
172+
stack_stx: {
173+
auth_id: number; // 123456789,
174+
burn_block_height: number; // 121,
175+
burn_header_hash: string; // "71b87d20a688d5a23dc2915cd0cff2dd019f81801717a230caf58ee5fae6faf0",
176+
burn_txid: string; // "e5d9aa62315aadfe670a0180fa3687852830f50152461bfd393a1298add88842",
177+
max_amount: number; // 4500432000000000,
178+
num_cycles: number; // 6,
179+
reward_addr: string; // "tb1pf4x64urhdsdmadxxhv2wwjv6e3evy59auu2xaauu3vz3adxtskfschm453",
180+
sender: {
181+
address: string; // "ST1Z7V02CJRY3G5R2RDG7SFAZA8VGH0Y44NC2NAJN",
182+
address_hash_bytes: string; // "0x7e7d804c963c381702c3607cbd5f52370883c425",
183+
address_version: number; // 26
184+
};
185+
signer_key: string; // "033b67384665cbc3a36052a2d1c739a6cd1222cd451c499400c9d42e2041a56161",
186+
stacked_ustx: number; // 4500432000000000,
187+
vtxindex: number; // 3
188+
};
189+
}
190+
191+
type BurnchainOp = BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt | BurnchainOpStackStx;
172192

173193
export type CoreNodeEvent =
174194
| SmartContractEvent

src/event-stream/reader.ts

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2-
BurnchainOp,
3-
CoreNodeBlockMessage,
2+
BurnchainOpRegisterAssetFt,
3+
BurnchainOpRegisterAssetNft,
4+
BurnchainOpStackStx,
45
CoreNodeEvent,
56
CoreNodeEventType,
67
CoreNodeParsedTxMessage,
@@ -60,16 +61,15 @@ import {
6061
UIntCV,
6162
stringAsciiCV,
6263
hexToCV,
63-
AddressVersion,
6464
} from '@stacks/transactions';
65-
import { poxAddressToBtcAddress, poxAddressToTuple } from '@stacks/stacking';
65+
import { poxAddressToTuple } from '@stacks/stacking';
6666
import { c32ToB58 } from 'c32check';
6767
import { decodePoxSyntheticPrintEvent } from './pox-event-parsing';
6868
import { PoxContractIdentifiers, SyntheticPoxEventName } from '../pox-helpers';
6969
import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV';
7070
import { logger } from '../logger';
7171
import { bufferToHex, hexToBuffer } from '@hirosystems/api-toolkit';
72-
import { PoXAddressVersion } from '@stacks/stacking/dist/constants';
72+
import { hexToBytes } from '@stacks/common';
7373

7474
export function getTxSenderAddress(tx: DecodedTxResult): string {
7575
const txSender = tx.auth.origin_condition.signer.address;
@@ -86,7 +86,7 @@ export function getTxSponsorAddress(tx: DecodedTxResult): string | undefined {
8686

8787
function createSubnetTransactionFromL1RegisterAsset(
8888
chainId: ChainID,
89-
burnchainOp: BurnchainOp,
89+
burnchainOp: BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt,
9090
subnetEvent: SmartContractEvent,
9191
txId: string
9292
): DecodedTxResult {
@@ -436,6 +436,88 @@ function createTransactionFromCoreBtcStxLockEvent(
436436
return tx;
437437
}
438438

439+
function createTransactionFromCoreBtcStxLockEventPox4(
440+
chainId: ChainID,
441+
burnOpData: BurnchainOpStackStx,
442+
txResult: string,
443+
txId: string
444+
): DecodedTxResult {
445+
const resultCv = decodeClarityValue<
446+
ClarityValueResponse<
447+
ClarityValueTuple<{
448+
'lock-amount': ClarityValueUInt;
449+
'unlock-burn-height': ClarityValueUInt;
450+
stacker: ClarityValuePrincipalStandard;
451+
}>
452+
>
453+
>(txResult);
454+
if (resultCv.type_id !== ClarityTypeID.ResponseOk) {
455+
throw new Error(`Unexpected tx result Clarity type ID: ${resultCv.type_id}`);
456+
}
457+
const senderAddress = decodeStacksAddress(burnOpData.stack_stx.sender.address);
458+
const poxAddressString =
459+
getChainIDNetwork(chainId) === 'mainnet'
460+
? BootContractAddress.mainnet
461+
: BootContractAddress.testnet;
462+
const poxAddress = decodeStacksAddress(poxAddressString);
463+
const contractName = 'pox-4';
464+
465+
const legacyClarityVals = [
466+
uintCV(burnOpData.stack_stx.stacked_ustx), // (amount-ustx uint)
467+
poxAddressToTuple(burnOpData.stack_stx.reward_addr), // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32))))
468+
uintCV(burnOpData.stack_stx.burn_block_height), // (start-burn-ht uint)
469+
uintCV(burnOpData.stack_stx.num_cycles), // (lock-period uint)
470+
noneCV(), // (signer-sig (optional (buff 65)))
471+
bufferCV(hexToBytes(burnOpData.stack_stx.signer_key)), // (signer-key (buff 33))
472+
uintCV(burnOpData.stack_stx.max_amount), // (max-amount uint)
473+
uintCV(burnOpData.stack_stx.auth_id), // (auth-id uint)
474+
];
475+
const fnLenBuffer = Buffer.alloc(4);
476+
fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
477+
const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
478+
const rawFnArgs = bufferToHex(Buffer.concat([fnLenBuffer, ...serializedClarityValues]));
479+
const clarityFnArgs = decodeClarityValueList(rawFnArgs);
480+
481+
const tx: DecodedTxResult = {
482+
tx_id: txId,
483+
version:
484+
getChainIDNetwork(chainId) === 'mainnet'
485+
? TransactionVersion.Mainnet
486+
: TransactionVersion.Testnet,
487+
chain_id: chainId,
488+
auth: {
489+
type_id: PostConditionAuthFlag.Standard,
490+
origin_condition: {
491+
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
492+
signer: {
493+
address_version: senderAddress[0],
494+
address_hash_bytes: senderAddress[1],
495+
address: burnOpData.stack_stx.sender.address,
496+
},
497+
nonce: '0',
498+
tx_fee: '0',
499+
key_encoding: TxPublicKeyEncoding.Compressed,
500+
signature: '0x',
501+
},
502+
},
503+
anchor_mode: AnchorModeID.Any,
504+
post_condition_mode: PostConditionModeID.Allow,
505+
post_conditions: [],
506+
post_conditions_buffer: '0x0100000000',
507+
payload: {
508+
type_id: TxPayloadTypeID.ContractCall,
509+
address: poxAddressString,
510+
address_version: poxAddress[0],
511+
address_hash_bytes: poxAddress[1],
512+
contract_name: contractName,
513+
function_name: 'stack-stx',
514+
function_args: clarityFnArgs,
515+
function_args_buffer: rawFnArgs,
516+
},
517+
};
518+
return tx;
519+
}
520+
439521
/*
440522
;; Delegate to `delegate-to` the ability to stack from a given address.
441523
;; This method _does not_ lock the funds, rather, it allows the delegate
@@ -685,6 +767,20 @@ export function parseMessageTransaction(
685767
if (stxTransferEvent) {
686768
rawTx = createTransactionFromCoreBtcTxEvent(chainId, stxTransferEvent, coreTx.txid);
687769
txSender = stxTransferEvent.stx_transfer_event.sender;
770+
} else if (
771+
coreTx.burnchain_op &&
772+
'stack_stx' in coreTx.burnchain_op &&
773+
coreTx.burnchain_op.stack_stx.signer_key
774+
) {
775+
// This is a pox-4 stack-stx burnchain op
776+
const burnOpData = coreTx.burnchain_op.stack_stx;
777+
rawTx = createTransactionFromCoreBtcStxLockEventPox4(
778+
chainId,
779+
coreTx.burnchain_op,
780+
coreTx.raw_result,
781+
coreTx.txid
782+
);
783+
txSender = burnOpData.sender.address;
688784
} else if (stxLockEvent) {
689785
const stxStacksPoxEvent =
690786
poxEvent?.decodedEvent.name === SyntheticPoxEventName.StackStx
@@ -720,6 +816,7 @@ export function parseMessageTransaction(
720816
} else if (
721817
subnetEvents.length > 0 &&
722818
coreTx.burnchain_op &&
819+
'register_asset' in coreTx.burnchain_op &&
723820
coreTx.burnchain_op.register_asset
724821
) {
725822
rawTx = createSubnetTransactionFromL1RegisterAsset(

src/tests-2.5/pox-4-burnchain-stack-stx.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import {
55
TransactionEventsResponse,
66
TransactionEventStxLock,
77
} from '@stacks/stacks-blockchain-api-types';
8-
import { AnchorMode, makeSTXTokenTransfer } from '@stacks/transactions';
8+
import {
9+
AnchorMode,
10+
boolCV,
11+
bufferCV,
12+
makeContractCall,
13+
makeSTXTokenTransfer,
14+
stringAsciiCV,
15+
uintCV,
16+
} from '@stacks/transactions';
917
import { testnetKeys } from '../api/routes/debug';
1018
import { StacksCoreRpcClient } from '../core-rpc/client';
1119
import { ECPair } from '../ec-helpers';
@@ -29,17 +37,21 @@ import { StacksNetwork } from '@stacks/network';
2937
import { RPCClient } from 'rpc-bitcoin';
3038
import * as supertest from 'supertest';
3139
import { ClarityValueUInt, decodeClarityValue } from 'stacks-encoding-native-js';
32-
import { decodeBtcAddress } from '@stacks/stacking';
40+
import { decodeBtcAddress, poxAddressToTuple } from '@stacks/stacking';
3341
import { timeout } from '@hirosystems/api-toolkit';
42+
import { hexToBytes } from '@stacks/common';
3443

3544
// Perform Stack-STX operation on Bitcoin.
3645
// See https://github.com/stacksgov/sips/blob/0da29c6911c49c45e4125dbeaed58069854591eb/sips/sip-007/sip-007-stacking-consensus.md#stx-operations-on-bitcoin
37-
async function createPox2StackStx(args: {
46+
async function createPox4StackStx(args: {
3847
stxAmount: bigint;
3948
cycleCount: number;
4049
stackerAddress: string;
4150
bitcoinWif: string;
4251
poxAddrPayout: string;
52+
signerKey: string;
53+
maxAmount: bigint;
54+
authID: number;
4355
}) {
4456
const btcAccount = ECPair.fromWIF(args.bitcoinWif, btc.networks.regtest);
4557
const feeAmount = 0.0001;
@@ -97,14 +109,17 @@ async function createPox2StackStx(args: {
97109
});
98110

99111
// StackStxOp: this operation executes the stack-stx operation.
100-
// 0 2 3 19 20
101-
// |------|--|-----------------------------|---------|
102-
// magic op uSTX to lock (u128) cycles (u8)
112+
// 0 2 3 19 20 53 69 73
113+
// |------|--|-----------------------------|------------|-------------------|-------------------|-------------------------|
114+
// magic op uSTX to lock (u128) cycles (u8) signer key (optional) max_amount (optional u128) auth_id (optional u32)
103115
const stackStxOpTxPayload = Buffer.concat([
104116
Buffer.from('id'), // magic: 'id' ascii encoded (for krypton)
105117
Buffer.from('x'), // op: 'x' ascii encoded,
106118
Buffer.from(args.stxAmount.toString(16).padStart(32, '0'), 'hex'), // uSTX to lock (u128)
107119
Buffer.from([args.cycleCount]), // cycles (u8)
120+
Buffer.from(args.signerKey, 'hex'), // signer key (33 bytes)
121+
Buffer.from(args.maxAmount.toString(16).padStart(32, '0'), 'hex'), // max_amount (u128)
122+
Buffer.from(args.authID.toString(16).padStart(8, '0'), 'hex'), // auth_id (u32)
108123
]);
109124
const stackStxOpTxHex = new btc.Psbt({ network: btc.networks.regtest })
110125
.setVersion(1)
@@ -153,6 +168,8 @@ describe('PoX-4 - Stack using Bitcoin-chain stack ops', () => {
153168

154169
let testAccountBalance: bigint;
155170
const testAccountBtcBalance = 5;
171+
const testStackAuthID = 123456789;
172+
const cycleCount = 6;
156173
let testStackAmount: bigint;
157174

158175
let stxOpBtcTxs: {
@@ -247,15 +264,49 @@ describe('PoX-4 - Stack using Bitcoin-chain stack ops', () => {
247264
await standByUntilBurnBlock(poxInfo.next_cycle.reward_phase_start_block_height); // a good time to stack
248265
});
249266

250-
test('Stack via Bitcoin tx', async () => {
267+
test('Submit set-signer-key-authorization transaction', async () => {
251268
const poxInfo = await client.getPox();
252269
testStackAmount = BigInt(poxInfo.min_amount_ustx * 1.2);
253-
stxOpBtcTxs = await createPox2StackStx({
270+
const [contractAddress, contractName] = poxInfo.contract_id.split('.');
271+
const tx = await makeContractCall({
272+
senderKey: seedAccount.secretKey,
273+
contractAddress,
274+
contractName,
275+
functionName: 'set-signer-key-authorization',
276+
functionArgs: [
277+
poxAddressToTuple(poxAddrPayoutAccount.btcAddr), // (pox-addr { version: (buff 1), hashbytes: (buff 32)})
278+
uintCV(cycleCount), // (period uint)
279+
uintCV(poxInfo.current_cycle.id), // (reward-cycle uint)
280+
stringAsciiCV('stack-stx'), // (topic (string-ascii 14))
281+
bufferCV(hexToBytes(seedAccount.pubKey)), // (signer-key (buff 33))
282+
boolCV(true), // (allowed bool)
283+
uintCV(testStackAmount), // (max-amount uint)
284+
uintCV(testStackAuthID), // (auth-id uint)
285+
],
286+
network: testEnv.stacksNetwork,
287+
anchorMode: AnchorMode.OnChainOnly,
288+
fee: 10000,
289+
validateWithAbi: false,
290+
});
291+
const expectedTxId = '0x' + tx.txid();
292+
const sendResult = await testEnv.client.sendTransaction(Buffer.from(tx.serialize()));
293+
expect(sendResult.txId).toBe(expectedTxId);
294+
295+
// Wait for API to receive and ingest tx
296+
await standByForTxSuccess(expectedTxId);
297+
});
298+
299+
test('Stack via Bitcoin tx', async () => {
300+
const poxInfo = await client.getPox();
301+
stxOpBtcTxs = await createPox4StackStx({
254302
bitcoinWif: account.wif,
255303
stackerAddress: account.stxAddr,
256304
poxAddrPayout: poxAddrPayoutAccount.btcAddr,
257305
stxAmount: testStackAmount,
258-
cycleCount: 6,
306+
cycleCount: cycleCount,
307+
signerKey: seedAccount.pubKey,
308+
maxAmount: testStackAmount,
309+
authID: testStackAuthID,
259310
});
260311
});
261312

@@ -281,8 +332,7 @@ describe('PoX-4 - Stack using Bitcoin-chain stack ops', () => {
281332
await standByUntilBlock(curInfo.stacks_tip_height + 1);
282333
});
283334

284-
// TODO: this is blocked by a blockchain bug: https://github.com/stacks-network/stacks-core/issues/4282
285-
test.skip('Test synthetic STX tx', async () => {
335+
test('Test synthetic STX tx', async () => {
286336
const coreNodeBalance = await client.getAccount(account.stxAddr);
287337
const addressEventsResp = await supertest(api.server)
288338
.get(`/extended/v1/tx/events?address=${account.stxAddr}`)

0 commit comments

Comments
 (0)