Skip to content

Commit 37adcc7

Browse files
authored
fix(rosetta): use coinbase txs hash instead of stx_lock for forged unlock_transaction #760
1 parent 5f07d74 commit 37adcc7

File tree

4 files changed

+63
-21
lines changed

4 files changed

+63
-21
lines changed

src/api/controllers/db-controller.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,8 @@ export async function getRosettaBlockTransactionsFromDataStore(opts: {
489489
return { found: false };
490490
}
491491

492+
const unlockingEvents = await opts.db.getUnlockedAddressesAtBlock(blockQuery.result);
493+
492494
const transactions: RosettaTransaction[] = [];
493495

494496
for (const tx of txsQuery.result) {
@@ -503,8 +505,7 @@ export async function getRosettaBlockTransactionsFromDataStore(opts: {
503505
});
504506
events = eventsQuery.results;
505507
}
506-
507-
const operations = await getOperations(tx, opts.db, minerRewards, events);
508+
const operations = await getOperations(tx, opts.db, minerRewards, events, unlockingEvents);
508509
const txMemo = parseTransactionMemo(tx);
509510
const rosettaTx: RosettaTransaction = {
510511
transaction_identifier: { hash: tx.tx_id },
@@ -518,17 +519,6 @@ export async function getRosettaBlockTransactionsFromDataStore(opts: {
518519
transactions.push(rosettaTx);
519520
}
520521

521-
// Search for unlocking events
522-
const unlockingEvents = await opts.db.getUnlockedAddressesAtBlock(blockQuery.result);
523-
if (unlockingEvents.length > 0) {
524-
const operations: RosettaOperation[] = [];
525-
processUnlockingEvents(unlockingEvents, operations);
526-
transactions.push({
527-
transaction_identifier: { hash: unlockingEvents[0].tx_id }, // All unlocking events share the same tx_id
528-
operations: operations,
529-
});
530-
}
531-
532522
return { found: true, result: transactions };
533523
}
534524

src/datastore/postgres-store.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5855,25 +5855,38 @@ export class PgDataStore
58555855
const lockQuery = await client.query<{
58565856
locked_amount: string;
58575857
unlock_height: string;
5858-
block_height: string;
58595858
locked_address: string;
58605859
tx_id: Buffer;
58615860
}>(
58625861
`
5863-
SELECT locked_amount, unlock_height, block_height, tx_id, locked_address
5862+
SELECT locked_amount, unlock_height, locked_address
58645863
FROM stx_lock_events
5865-
WHERE canonical = true AND unlock_height <= $1 AND unlock_height > $2
5864+
WHERE microblock_canonical = true AND canonical = true
5865+
AND unlock_height <= $1 AND unlock_height > $2
58665866
`,
58675867
[current_burn_height, previous_burn_height]
58685868
);
58695869

5870+
const txIdQuery = await client.query<{
5871+
tx_id: Buffer;
5872+
}>(
5873+
`
5874+
SELECT tx_id
5875+
FROM txs
5876+
WHERE microblock_canonical = true AND canonical = true
5877+
AND block_height = $1 AND type_id = $2
5878+
LIMIT 1
5879+
`,
5880+
[block.block_height, DbTxTypeId.Coinbase]
5881+
);
5882+
58705883
const result: StxUnlockEvent[] = [];
58715884
lockQuery.rows.forEach(row => {
58725885
const unlockEvent: StxUnlockEvent = {
58735886
unlock_height: row.unlock_height,
58745887
unlocked_amount: row.locked_amount,
58755888
stacker_address: row.locked_address,
5876-
tx_id: bufferToHexPrefixString(row.tx_id),
5889+
tx_id: bufferToHexPrefixString(txIdQuery.rows[0].tx_id),
58775890
};
58785891
result.push(unlockEvent);
58795892
});

src/rosetta-helpers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ export async function getOperations(
110110
tx: DbTx | DbMempoolTx | BaseTx,
111111
db: DataStore,
112112
minerRewards?: DbMinerReward[],
113-
events?: DbEvent[]
113+
events?: DbEvent[],
114+
stxUnlockEvents?: StxUnlockEvent[]
114115
): Promise<RosettaOperation[]> {
115116
const operations: RosettaOperation[] = [];
116117
const txType = getTxTypeString(tx.type_id);
@@ -133,6 +134,9 @@ export async function getOperations(
133134
if (minerRewards !== undefined) {
134135
getMinerOperations(minerRewards, operations);
135136
}
137+
if (stxUnlockEvents && stxUnlockEvents?.length > 0) {
138+
processUnlockingEvents(stxUnlockEvents, operations);
139+
}
136140
break;
137141
case 'poison_microblock':
138142
operations.push(makePoisonMicroblockOperation(tx, 0));

src/tests-rosetta/api.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ import { makeSigHashPreSign, MessageSignature } from '@stacks/transactions';
7474
import { decodeBtcAddress } from '@stacks/stacking';
7575

7676

77-
7877
describe('Rosetta API', () => {
7978
let db: PgDataStore;
8079
let client: PoolClient;
@@ -2618,7 +2617,7 @@ describe('Rosetta API', () => {
26182617
expect(blockStxOpsQuery.status).toBe(200);
26192618
expect(blockStxOpsQuery.type).toBe('application/json');
26202619

2621-
let stxUnlockHeight = blockStxOpsQuery.body.block.transactions[1].operations[1].metadata.unlock_burn_height;
2620+
let stxUnlockHeight = Number.parseInt(blockStxOpsQuery.body.block.transactions[1].operations[1].metadata.unlock_burn_height);
26222621
expect(stxUnlockHeight).toBeTruthy();
26232622

26242623
const blockTxOpsQuery = await supertest(api.address)
@@ -2671,6 +2670,7 @@ describe('Rosetta API', () => {
26712670
expect(blockTxOpsQuery.body.operations).toContainEqual(expect.objectContaining(expectedStxLockOp));
26722671

26732672
let current_burn_block_height = block.result.burn_block_height;
2673+
//wait for the unlock block height
26742674
while(current_burn_block_height < stxUnlockHeight){
26752675
block = await db.getCurrentBlock();
26762676
assert(block.found);
@@ -2684,9 +2684,44 @@ describe('Rosetta API', () => {
26842684
network_identifier: { blockchain: 'stacks', network: 'testnet' },
26852685
block_identifier: { hash: block.result.block_hash },
26862686
});
2687+
26872688
expect(query1.status).toBe(200);
26882689
expect(query1.type).toBe('application/json');
2689-
expect(JSON.stringify(query1.body)).toMatch(/"stx_unlock"/)
2690+
expect(query1.body.block.transactions[0].operations[0].type).toBe('coinbase');
2691+
expect(query1.body.block.transactions[0].operations[1].type).toBe('stx_unlock');
2692+
2693+
const query2 = await supertest(api.address)
2694+
.post(`/rosetta/v1/block/transaction`)
2695+
.send({
2696+
network_identifier: { blockchain: 'stacks', network: 'testnet' },
2697+
block_identifier: { hash: block.result.block_hash },
2698+
transaction_identifier: {hash: query1.body.block.transactions[0].transaction_identifier.hash }
2699+
});
2700+
2701+
const expectedResponse = {
2702+
operation_identifier: {
2703+
index: 1,
2704+
},
2705+
type: "stx_unlock",
2706+
status: "success",
2707+
account: {
2708+
address: testnetKeys[0].stacksAddress,
2709+
},
2710+
amount: {
2711+
value: stacking_amount,
2712+
currency: {
2713+
decimals: 6,
2714+
symbol: "STX"
2715+
}
2716+
},
2717+
metadata: {
2718+
tx_id: query1.body.block.transactions[0].transaction_identifier.hash,
2719+
}
2720+
}
2721+
expect(query2.status).toBe(200);
2722+
expect(query2.type).toBe('application/json');
2723+
expect(query2.body.operations[1]).toStrictEqual(expectedResponse);
2724+
26902725
})
26912726

26922727
test('delegate-stacking rosetta transaction cycle', async() => {

0 commit comments

Comments
 (0)