Skip to content

Commit

Permalink
fix: pox4 stack-stx burn-op handling (#1936)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zone117x authored Apr 11, 2024
1 parent b0e5720 commit 9e9a464
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 20 deletions.
26 changes: 23 additions & 3 deletions src/event-stream/core-node-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ interface FtBurnEvent extends CoreNodeEventBase {
};
}

interface BurnchainOpRegisterAssetNft {
export interface BurnchainOpRegisterAssetNft {
register_asset: {
asset_type: 'nft';
burn_header_hash: string;
Expand All @@ -158,7 +158,7 @@ interface BurnchainOpRegisterAssetNft {
};
}

interface BurnchainOpRegisterAssetFt {
export interface BurnchainOpRegisterAssetFt {
register_asset: {
asset_type: 'ft';
burn_header_hash: string;
Expand All @@ -168,7 +168,27 @@ interface BurnchainOpRegisterAssetFt {
};
}

export type BurnchainOp = BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt;
export interface BurnchainOpStackStx {
stack_stx: {
auth_id: number; // 123456789,
burn_block_height: number; // 121,
burn_header_hash: string; // "71b87d20a688d5a23dc2915cd0cff2dd019f81801717a230caf58ee5fae6faf0",
burn_txid: string; // "e5d9aa62315aadfe670a0180fa3687852830f50152461bfd393a1298add88842",
max_amount: number; // 4500432000000000,
num_cycles: number; // 6,
reward_addr: string; // "tb1pf4x64urhdsdmadxxhv2wwjv6e3evy59auu2xaauu3vz3adxtskfschm453",
sender: {
address: string; // "ST1Z7V02CJRY3G5R2RDG7SFAZA8VGH0Y44NC2NAJN",
address_hash_bytes: string; // "0x7e7d804c963c381702c3607cbd5f52370883c425",
address_version: number; // 26
};
signer_key: string; // "033b67384665cbc3a36052a2d1c739a6cd1222cd451c499400c9d42e2041a56161",
stacked_ustx: number; // 4500432000000000,
vtxindex: number; // 3
};
}

type BurnchainOp = BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt | BurnchainOpStackStx;

export type CoreNodeEvent =
| SmartContractEvent
Expand Down
109 changes: 103 additions & 6 deletions src/event-stream/reader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BurnchainOp,
CoreNodeBlockMessage,
BurnchainOpRegisterAssetFt,
BurnchainOpRegisterAssetNft,
BurnchainOpStackStx,
CoreNodeEvent,
CoreNodeEventType,
CoreNodeParsedTxMessage,
Expand Down Expand Up @@ -60,16 +61,15 @@ import {
UIntCV,
stringAsciiCV,
hexToCV,
AddressVersion,
} from '@stacks/transactions';
import { poxAddressToBtcAddress, poxAddressToTuple } from '@stacks/stacking';
import { poxAddressToTuple } from '@stacks/stacking';
import { c32ToB58 } from 'c32check';
import { decodePoxSyntheticPrintEvent } from './pox-event-parsing';
import { PoxContractIdentifiers, SyntheticPoxEventName } from '../pox-helpers';
import { principalCV } from '@stacks/transactions/dist/clarity/types/principalCV';
import { logger } from '../logger';
import { bufferToHex, hexToBuffer } from '@hirosystems/api-toolkit';
import { PoXAddressVersion } from '@stacks/stacking/dist/constants';
import { hexToBytes } from '@stacks/common';

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

function createSubnetTransactionFromL1RegisterAsset(
chainId: ChainID,
burnchainOp: BurnchainOp,
burnchainOp: BurnchainOpRegisterAssetNft | BurnchainOpRegisterAssetFt,
subnetEvent: SmartContractEvent,
txId: string
): DecodedTxResult {
Expand Down Expand Up @@ -436,6 +436,88 @@ function createTransactionFromCoreBtcStxLockEvent(
return tx;
}

function createTransactionFromCoreBtcStxLockEventPox4(
chainId: ChainID,
burnOpData: BurnchainOpStackStx,
txResult: string,
txId: string
): DecodedTxResult {
const resultCv = decodeClarityValue<
ClarityValueResponse<
ClarityValueTuple<{
'lock-amount': ClarityValueUInt;
'unlock-burn-height': ClarityValueUInt;
stacker: ClarityValuePrincipalStandard;
}>
>
>(txResult);
if (resultCv.type_id !== ClarityTypeID.ResponseOk) {
throw new Error(`Unexpected tx result Clarity type ID: ${resultCv.type_id}`);
}
const senderAddress = decodeStacksAddress(burnOpData.stack_stx.sender.address);
const poxAddressString =
getChainIDNetwork(chainId) === 'mainnet'
? BootContractAddress.mainnet
: BootContractAddress.testnet;
const poxAddress = decodeStacksAddress(poxAddressString);
const contractName = 'pox-4';

const legacyClarityVals = [
uintCV(burnOpData.stack_stx.stacked_ustx), // (amount-ustx uint)
poxAddressToTuple(burnOpData.stack_stx.reward_addr), // (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32))))
uintCV(burnOpData.stack_stx.burn_block_height), // (start-burn-ht uint)
uintCV(burnOpData.stack_stx.num_cycles), // (lock-period uint)
noneCV(), // (signer-sig (optional (buff 65)))
bufferCV(hexToBytes(burnOpData.stack_stx.signer_key)), // (signer-key (buff 33))
uintCV(burnOpData.stack_stx.max_amount), // (max-amount uint)
uintCV(burnOpData.stack_stx.auth_id), // (auth-id uint)
];
const fnLenBuffer = Buffer.alloc(4);
fnLenBuffer.writeUInt32BE(legacyClarityVals.length);
const serializedClarityValues = legacyClarityVals.map(c => serializeCV(c));
const rawFnArgs = bufferToHex(Buffer.concat([fnLenBuffer, ...serializedClarityValues]));
const clarityFnArgs = decodeClarityValueList(rawFnArgs);

const tx: DecodedTxResult = {
tx_id: txId,
version:
getChainIDNetwork(chainId) === 'mainnet'
? TransactionVersion.Mainnet
: TransactionVersion.Testnet,
chain_id: chainId,
auth: {
type_id: PostConditionAuthFlag.Standard,
origin_condition: {
hash_mode: TxSpendingConditionSingleSigHashMode.P2PKH,
signer: {
address_version: senderAddress[0],
address_hash_bytes: senderAddress[1],
address: burnOpData.stack_stx.sender.address,
},
nonce: '0',
tx_fee: '0',
key_encoding: TxPublicKeyEncoding.Compressed,
signature: '0x',
},
},
anchor_mode: AnchorModeID.Any,
post_condition_mode: PostConditionModeID.Allow,
post_conditions: [],
post_conditions_buffer: '0x0100000000',
payload: {
type_id: TxPayloadTypeID.ContractCall,
address: poxAddressString,
address_version: poxAddress[0],
address_hash_bytes: poxAddress[1],
contract_name: contractName,
function_name: 'stack-stx',
function_args: clarityFnArgs,
function_args_buffer: rawFnArgs,
},
};
return tx;
}

/*
;; Delegate to `delegate-to` the ability to stack from a given address.
;; This method _does not_ lock the funds, rather, it allows the delegate
Expand Down Expand Up @@ -685,6 +767,20 @@ export function parseMessageTransaction(
if (stxTransferEvent) {
rawTx = createTransactionFromCoreBtcTxEvent(chainId, stxTransferEvent, coreTx.txid);
txSender = stxTransferEvent.stx_transfer_event.sender;
} else if (
coreTx.burnchain_op &&
'stack_stx' in coreTx.burnchain_op &&
coreTx.burnchain_op.stack_stx.signer_key
) {
// This is a pox-4 stack-stx burnchain op
const burnOpData = coreTx.burnchain_op.stack_stx;
rawTx = createTransactionFromCoreBtcStxLockEventPox4(
chainId,
coreTx.burnchain_op,
coreTx.raw_result,
coreTx.txid
);
txSender = burnOpData.sender.address;
} else if (stxLockEvent) {
const stxStacksPoxEvent =
poxEvent?.decodedEvent.name === SyntheticPoxEventName.StackStx
Expand Down Expand Up @@ -720,6 +816,7 @@ export function parseMessageTransaction(
} else if (
subnetEvents.length > 0 &&
coreTx.burnchain_op &&
'register_asset' in coreTx.burnchain_op &&
coreTx.burnchain_op.register_asset
) {
rawTx = createSubnetTransactionFromL1RegisterAsset(
Expand Down
72 changes: 61 additions & 11 deletions src/tests-2.5/pox-4-burnchain-stack-stx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import {
TransactionEventsResponse,
TransactionEventStxLock,
} from '@stacks/stacks-blockchain-api-types';
import { AnchorMode, makeSTXTokenTransfer } from '@stacks/transactions';
import {
AnchorMode,
boolCV,
bufferCV,
makeContractCall,
makeSTXTokenTransfer,
stringAsciiCV,
uintCV,
} from '@stacks/transactions';
import { testnetKeys } from '../api/routes/debug';
import { StacksCoreRpcClient } from '../core-rpc/client';
import { ECPair } from '../ec-helpers';
Expand All @@ -29,17 +37,21 @@ import { StacksNetwork } from '@stacks/network';
import { RPCClient } from 'rpc-bitcoin';
import * as supertest from 'supertest';
import { ClarityValueUInt, decodeClarityValue } from 'stacks-encoding-native-js';
import { decodeBtcAddress } from '@stacks/stacking';
import { decodeBtcAddress, poxAddressToTuple } from '@stacks/stacking';
import { timeout } from '@hirosystems/api-toolkit';
import { hexToBytes } from '@stacks/common';

// Perform Stack-STX operation on Bitcoin.
// See https://github.com/stacksgov/sips/blob/0da29c6911c49c45e4125dbeaed58069854591eb/sips/sip-007/sip-007-stacking-consensus.md#stx-operations-on-bitcoin
async function createPox2StackStx(args: {
async function createPox4StackStx(args: {
stxAmount: bigint;
cycleCount: number;
stackerAddress: string;
bitcoinWif: string;
poxAddrPayout: string;
signerKey: string;
maxAmount: bigint;
authID: number;
}) {
const btcAccount = ECPair.fromWIF(args.bitcoinWif, btc.networks.regtest);
const feeAmount = 0.0001;
Expand Down Expand Up @@ -97,14 +109,17 @@ async function createPox2StackStx(args: {
});

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

let testAccountBalance: bigint;
const testAccountBtcBalance = 5;
const testStackAuthID = 123456789;
const cycleCount = 6;
let testStackAmount: bigint;

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

test('Stack via Bitcoin tx', async () => {
test('Submit set-signer-key-authorization transaction', async () => {
const poxInfo = await client.getPox();
testStackAmount = BigInt(poxInfo.min_amount_ustx * 1.2);
stxOpBtcTxs = await createPox2StackStx({
const [contractAddress, contractName] = poxInfo.contract_id.split('.');
const tx = await makeContractCall({
senderKey: seedAccount.secretKey,
contractAddress,
contractName,
functionName: 'set-signer-key-authorization',
functionArgs: [
poxAddressToTuple(poxAddrPayoutAccount.btcAddr), // (pox-addr { version: (buff 1), hashbytes: (buff 32)})
uintCV(cycleCount), // (period uint)
uintCV(poxInfo.current_cycle.id), // (reward-cycle uint)
stringAsciiCV('stack-stx'), // (topic (string-ascii 14))
bufferCV(hexToBytes(seedAccount.pubKey)), // (signer-key (buff 33))
boolCV(true), // (allowed bool)
uintCV(testStackAmount), // (max-amount uint)
uintCV(testStackAuthID), // (auth-id uint)
],
network: testEnv.stacksNetwork,
anchorMode: AnchorMode.OnChainOnly,
fee: 10000,
validateWithAbi: false,
});
const expectedTxId = '0x' + tx.txid();
const sendResult = await testEnv.client.sendTransaction(Buffer.from(tx.serialize()));
expect(sendResult.txId).toBe(expectedTxId);

// Wait for API to receive and ingest tx
await standByForTxSuccess(expectedTxId);
});

test('Stack via Bitcoin tx', async () => {
const poxInfo = await client.getPox();
stxOpBtcTxs = await createPox4StackStx({
bitcoinWif: account.wif,
stackerAddress: account.stxAddr,
poxAddrPayout: poxAddrPayoutAccount.btcAddr,
stxAmount: testStackAmount,
cycleCount: 6,
cycleCount: cycleCount,
signerKey: seedAccount.pubKey,
maxAmount: testStackAmount,
authID: testStackAuthID,
});
});

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

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

0 comments on commit 9e9a464

Please sign in to comment.