diff --git a/src/event-stream/core-node-message.ts b/src/event-stream/core-node-message.ts index 8250fc265..066e0ffe6 100644 --- a/src/event-stream/core-node-message.ts +++ b/src/event-stream/core-node-message.ts @@ -326,7 +326,7 @@ export interface CoreNodeBlockMessage { missed_reward_slots: []; }; }; - block_time: number; + block_time: number | null; signer_bitvec?: string | null; signer_signature?: string[]; } diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 6d36ff7da..00b541eba 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -997,20 +997,20 @@ export function parseNewBlockMessage( ) { const counts = newCoreNoreBlockEventCounts(); + // Nakamoto blocks now include their own `block_time`, but this will be empty for pre-Nakamoto + // blocks. We'll use the parent burn block timestamp as the receipt date for those. If both are + // blank, there's something wrong with Stacks core. + const block_time = msg.block_time ?? msg.burn_block_time; + if (block_time === undefined || block_time === null) { + throw new Error('Block message has no block_time or burn_block_time'); + } + const parsedTxs: CoreNodeParsedTxMessage[] = []; const blockData: CoreNodeMsgBlockData = { ...msg, + block_time, }; - if (!blockData.block_time) { - // If running in IBD mode, we use the parent burn block timestamp as the receipt date, - // otherwise, use the current timestamp. - const stacksBlockReceiptDate = isEventReplay - ? msg.burn_block_time - : Math.round(Date.now() / 1000); - blockData.block_time = stacksBlockReceiptDate; - } - msg.transactions.forEach(item => { const parsedTx = parseMessageTransaction(chainId, item, blockData, msg.events); if (parsedTx) { diff --git a/tests/api/block-time.test.ts b/tests/api/block-time.test.ts new file mode 100644 index 000000000..b4607b9a6 --- /dev/null +++ b/tests/api/block-time.test.ts @@ -0,0 +1,82 @@ +import { ChainID } from '@stacks/transactions'; +import { CoreNodeBlockMessage } from '../../src/event-stream/core-node-message'; +import { parseNewBlockMessage } from '../../src/event-stream/event-server'; + +describe('block time tests', () => { + test('takes block_time from block header', () => { + const block: CoreNodeBlockMessage = { + block_time: 1716238792, + block_height: 1, + block_hash: '0x1234', + index_block_hash: '0x5678', + parent_index_block_hash: '0x9abc', + parent_block_hash: '0x1234', + parent_microblock: '0x1234', + parent_microblock_sequence: 0, + parent_burn_block_hash: '0x1234', + parent_burn_block_height: 0, + parent_burn_block_timestamp: 0, + burn_block_time: 1234567890, + burn_block_hash: '0x1234', + burn_block_height: 1, + miner_txid: '0x1234', + events: [], + transactions: [], + matured_miner_rewards: [], + }; + const { dbData: parsed } = parseNewBlockMessage(ChainID.Mainnet, block, false); + expect(parsed.block.block_time).toEqual(1716238792); // Takes block_time from block header + }); + + test('takes burn_block_time from block header when block_time is not present', () => { + const block: CoreNodeBlockMessage = { + block_time: null, + block_height: 1, + block_hash: '0x1234', + index_block_hash: '0x5678', + parent_index_block_hash: '0x9abc', + parent_block_hash: '0x1234', + parent_microblock: '0x1234', + parent_microblock_sequence: 0, + parent_burn_block_hash: '0x1234', + parent_burn_block_height: 0, + parent_burn_block_timestamp: 0, + burn_block_time: 1234567890, + burn_block_hash: '0x1234', + burn_block_height: 1, + miner_txid: '0x1234', + events: [], + transactions: [], + matured_miner_rewards: [], + }; + const { dbData: parsed } = parseNewBlockMessage(ChainID.Mainnet, block, false); + expect(parsed.block.block_time).toEqual(1234567890); // Takes burn_block_time from block header + }); + + test('throws error if block_time and burn_block_time are not present', () => { + // Use `any` to avoid type errors when setting `block_time` and `burn_block_time` to `null`. + const block: any = { + block_time: null, + burn_block_time: null, + block_height: 1, + block_hash: '0x1234', + index_block_hash: '0x5678', + parent_index_block_hash: '0x9abc', + parent_block_hash: '0x1234', + parent_microblock: '0x1234', + parent_microblock_sequence: 0, + parent_burn_block_hash: '0x1234', + parent_burn_block_height: 0, + parent_burn_block_timestamp: 0, + burn_block_hash: '0x1234', + burn_block_height: 1, + miner_txid: '0x1234', + events: [], + transactions: [], + matured_miner_rewards: [], + }; + expect(() => parseNewBlockMessage(ChainID.Mainnet, block, false)).toThrow( + 'Block message has no block_time or burn_block_time' + ); + }); +}); diff --git a/tests/api/synthetic-stx-txs.test.ts b/tests/api/synthetic-stx-txs.test.ts index 6d961f1da..b19769c30 100644 --- a/tests/api/synthetic-stx-txs.test.ts +++ b/tests/api/synthetic-stx-txs.test.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { DecodedTxResult, TxPayloadTypeID } from 'stacks-encoding-native-js'; import { CoreNodeBlockMessage } from '../../src/event-stream/core-node-message'; -import { parseMessageTransaction } from '../../src/event-stream/reader'; +import { CoreNodeMsgBlockData, parseMessageTransaction } from '../../src/event-stream/reader'; import { parseNewBlockMessage } from '../../src/event-stream/event-server'; // Test processing of the psuedo-Stacks transactions, i.e. the ones that @@ -20,7 +20,12 @@ describe('synthetic stx txs', () => { if (!txMsg) { throw new Error(`Cound not find tx ${txid}`); } - const parsed = parseMessageTransaction(ChainID.Mainnet, txMsg, blockMsg, blockMsg.events); + const parsed = parseMessageTransaction( + ChainID.Mainnet, + txMsg, + blockMsg as unknown as CoreNodeMsgBlockData, + blockMsg.events + ); if (!parsed) { throw new Error(`Failed to parse ${txid}`); } @@ -75,7 +80,12 @@ describe('synthetic stx txs', () => { if (!txMsg) { throw new Error(`Cound not find tx ${txid}`); } - const parsed = parseMessageTransaction(ChainID.Mainnet, txMsg, blockMsg, blockMsg.events); + const parsed = parseMessageTransaction( + ChainID.Mainnet, + txMsg, + blockMsg as unknown as CoreNodeMsgBlockData, + blockMsg.events + ); if (!parsed) { throw new Error(`Failed to parse ${txid}`); } @@ -130,7 +140,12 @@ describe('synthetic stx txs', () => { if (!txMsg) { throw new Error(`Cound not find tx ${txid}`); } - const parsed = parseMessageTransaction(ChainID.Mainnet, txMsg, blockMsg, blockMsg.events); + const parsed = parseMessageTransaction( + ChainID.Mainnet, + txMsg, + blockMsg as unknown as CoreNodeMsgBlockData, + blockMsg.events + ); if (!parsed) { throw new Error(`Failed to parse ${txid}`); } @@ -234,7 +249,12 @@ describe('synthetic stx txs', () => { if (!txMsg) { throw new Error(`Cound not find tx ${txid}`); } - const parsed = parseMessageTransaction(ChainID.Mainnet, txMsg, blockMsg, blockMsg.events); + const parsed = parseMessageTransaction( + ChainID.Mainnet, + txMsg, + blockMsg as unknown as CoreNodeMsgBlockData, + blockMsg.events + ); if (!parsed) { throw new Error(`Failed to parse ${txid}`); } @@ -333,7 +353,7 @@ describe('synthetic stx txs', () => { const parsed = parseMessageTransaction( ChainID.Mainnet, payload.txMsg, - payload.blockMsg, + payload.blockMsg as unknown as CoreNodeMsgBlockData, payload.blockMsg.events ); let txType: 'contract_call' | 'token_transfer' | null;