Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: pox4 stack-stx burn-op handling #1936

Merged
merged 4 commits into from
Apr 11, 2024
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
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)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for burn ops we always need to set-signer-key-authorization first, there's never a signature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think so

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
Loading