From 5419a8992c7c890755884bdeaffb1df3d057bc7e Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Thu, 2 Oct 2025 22:40:48 -0600 Subject: [PATCH 1/5] fix: reorg burnchain rewards --- src/datastore/pg-write-store.ts | 34 ++++++-------------------------- src/event-stream/event-server.ts | 2 -- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index f95ee8be0..97e3d4ea2 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -1923,35 +1923,8 @@ export class PgWriteStore extends PgStore { }; } - async updateBurnchainRewards({ - burnchainBlockHash, - burnchainBlockHeight, - rewards, - }: { - burnchainBlockHash: string; - burnchainBlockHeight: number; - rewards: DbBurnchainReward[]; - }): Promise { + async updateBurnchainRewards({ rewards }: { rewards: DbBurnchainReward[] }): Promise { return await this.sqlWriteTransaction(async sql => { - const existingRewards = await sql< - { - reward_recipient: string; - reward_amount: string; - }[] - >` - UPDATE burnchain_rewards - SET canonical = false - WHERE canonical = true AND - (burn_block_hash = ${burnchainBlockHash} - OR burn_block_height >= ${burnchainBlockHeight}) - `; - - if (existingRewards.count > 0) { - logger.warn( - `Invalidated ${existingRewards.count} burnchain rewards after fork detected at burnchain block ${burnchainBlockHash}` - ); - } - for (const reward of rewards) { const values: BurnchainRewardInsertValues = { canonical: true, @@ -3607,6 +3580,11 @@ export class PgWriteStore extends PgStore { if (orphanedBlockResult.length > 0) { const orphanedBlocks = orphanedBlockResult.map(b => parseBlockQueryResult(b)); for (const orphanedBlock of orphanedBlocks) { + await sql` + UPDATE burnchain_rewards + SET canonical = false + WHERE canonical = true AND burn_block_hash = ${orphanedBlock.burn_block_hash} + `; const microCanonicalUpdateResult = await this.updateMicroCanonical(sql, { isCanonical: false, blockHeight: orphanedBlock.block_height, diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index b02e41087..3b22a0936 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -121,8 +121,6 @@ async function handleBurnBlockMessage( return slotHolder; }); await db.updateBurnchainRewards({ - burnchainBlockHash: burnBlockMsg.burn_block_hash, - burnchainBlockHeight: burnBlockMsg.burn_block_height, rewards: rewards, }); await db.updateBurnchainRewardSlotHolders({ From 108cb5797a6b6a4f5cf396b704e9baa92e963a43 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Thu, 2 Oct 2025 22:49:01 -0600 Subject: [PATCH 2/5] fix: migration --- .../1759466478081_burnchain-rewards-reorg.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 migrations/1759466478081_burnchain-rewards-reorg.js diff --git a/migrations/1759466478081_burnchain-rewards-reorg.js b/migrations/1759466478081_burnchain-rewards-reorg.js new file mode 100644 index 000000000..253b6b2bf --- /dev/null +++ b/migrations/1759466478081_burnchain-rewards-reorg.js @@ -0,0 +1,21 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = pgm => { + pgm.sql(` + WITH burn_blocks AS ( + SELECT DISTINCT ON (burn_block_height) burn_block_hash, canonical + FROM blocks + ORDER BY burn_block_height DESC, block_height DESC + ) + UPDATE burnchain_rewards + SET canonical = ( + SELECT canonical + FROM burn_blocks + WHERE burnchain_rewards.burn_block_hash = burn_blocks.burn_block_hash + ) + `); +}; + +exports.down = pgm => {}; From 4c511f87d75d41b2d8be86cdafab7e5ec22f14ee Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Fri, 3 Oct 2025 10:56:28 -0600 Subject: [PATCH 3/5] fix: tests --- tests/api/burnchain.test.ts | 18 ---- tests/api/datastore.test.ts | 197 +++++++++++++++++++++++------------- 2 files changed, 129 insertions(+), 86 deletions(-) diff --git a/tests/api/burnchain.test.ts b/tests/api/burnchain.test.ts index f811a46a3..fd209d936 100644 --- a/tests/api/burnchain.test.ts +++ b/tests/api/burnchain.test.ts @@ -201,13 +201,9 @@ describe('burnchain tests', () => { reward_index: 2, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1, reward2], }); await db.updateBurnchainRewards({ - burnchainBlockHash: reward3.burn_block_hash, - burnchainBlockHeight: reward3.burn_block_height, rewards: [reward3, reward4, reward5], }); @@ -282,18 +278,12 @@ describe('burnchain tests', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1], }); await db.updateBurnchainRewards({ - burnchainBlockHash: reward2.burn_block_hash, - burnchainBlockHeight: reward2.burn_block_height, rewards: [reward2], }); await db.updateBurnchainRewards({ - burnchainBlockHash: reward3.burn_block_hash, - burnchainBlockHeight: reward3.burn_block_height, rewards: [reward3], }); const rewardResult = await supertest(api.server).get( @@ -320,8 +310,6 @@ describe('burnchain tests', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1], }); const rewardResult = await supertest(api.server).get(`/extended/v1/burnchain/rewards/${addr1}`); @@ -360,8 +348,6 @@ describe('burnchain tests', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1], }); const rewardResult = await supertest(api.server).get( @@ -402,8 +388,6 @@ describe('burnchain tests', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1], }); const rewardResult = await supertest(api.server).get( @@ -444,8 +428,6 @@ describe('burnchain tests', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1], }); const rewardResult = await supertest(api.server).get( diff --git a/tests/api/datastore.test.ts b/tests/api/datastore.test.ts index 909a8d495..6b2cd6116 100644 --- a/tests/api/datastore.test.ts +++ b/tests/api/datastore.test.ts @@ -3043,13 +3043,9 @@ describe('postgres datastore', () => { reward_index: 0, }; await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, rewards: [reward1, reward2], }); await db.updateBurnchainRewards({ - burnchainBlockHash: reward3.burn_block_hash, - burnchainBlockHeight: reward3.burn_block_height, rewards: [reward3], }); const rewardQuery = await db.getBurnchainRewards({ @@ -3091,80 +3087,145 @@ describe('postgres datastore', () => { test('pg burnchain reward reorg handling', async () => { const addr1 = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ'; const addr2 = '1DDUAqoyXvhF4cxznN9uL6j9ok1oncsT2z'; - const reward1: DbBurnchainReward = { - canonical: true, - burn_block_hash: '0x1234', - burn_block_height: 200, - burn_amount: 2000n, - reward_recipient: addr1, - reward_amount: 900n, - reward_index: 0, - }; - const reward2: DbBurnchainReward = { - canonical: true, - burn_block_hash: '0x1234', + + const mineTenure = async (args: { + block_height: number; + block_hash: string; + index_block_hash: string; + parent_index_block_hash: string; + parent_block_hash: string; + burn_block_hash: string; + burn_block_height: number; + tenure_height: number; + }) => { + // Add some rewards + await db.updateBurnchainRewards({ + rewards: [ + { + canonical: true, + burn_block_hash: args.burn_block_hash, + burn_block_height: args.burn_block_height, + burn_amount: 2000n, + reward_recipient: addr1, + reward_amount: 900n, + reward_index: 0, + }, + { + canonical: true, + burn_block_hash: args.burn_block_hash, + burn_block_height: args.burn_block_height, + burn_amount: 2001n, + reward_recipient: addr2, + reward_amount: 901n, + reward_index: 1, + }, + ], + }); + // Mine a tenure based on that burn block + await db.update( + new TestBlockBuilder({ + block_height: args.block_height, + block_hash: args.block_hash, + index_block_hash: args.index_block_hash, + parent_index_block_hash: args.parent_index_block_hash, + parent_block_hash: args.parent_block_hash, + burn_block_hash: args.burn_block_hash, + burn_block_height: args.burn_block_height, + tenure_height: args.tenure_height, + }).build() + ); + }; + + // Mine 3 tenures, check rewards + await mineTenure({ + block_height: 1, + block_hash: '0x11', + index_block_hash: '0x11', + parent_index_block_hash: '0x0000', + parent_block_hash: '0x0000', + burn_block_hash: '0x1111', burn_block_height: 200, - burn_amount: 2001n, - reward_recipient: addr1, - reward_amount: 901n, - reward_index: 1, - }; - const reward3: DbBurnchainReward = { - canonical: true, - burn_block_hash: '0x2345', - burn_block_height: 201, - burn_amount: 3001n, - reward_recipient: addr1, - reward_amount: 902n, - reward_index: 0, - }; - // block that triggers a reorg of all previous - const reward4: DbBurnchainReward = { - canonical: true, - burn_block_hash: reward1.burn_block_hash, - burn_block_height: reward1.burn_block_height, - burn_amount: 4001n, - reward_recipient: addr2, - reward_amount: 903n, - reward_index: 0, - }; - await db.updateBurnchainRewards({ - burnchainBlockHash: reward1.burn_block_hash, - burnchainBlockHeight: reward1.burn_block_height, - rewards: [reward1, reward2], + tenure_height: 1, }); - await db.updateBurnchainRewards({ - burnchainBlockHash: reward3.burn_block_hash, - burnchainBlockHeight: reward3.burn_block_height, - rewards: [reward3], + await mineTenure({ + block_height: 2, + block_hash: '0x22', + index_block_hash: '0x22', + parent_index_block_hash: '0x11', + parent_block_hash: '0x11', + burn_block_hash: '0x1112', + burn_block_height: 201, + tenure_height: 2, }); - await db.updateBurnchainRewards({ - burnchainBlockHash: reward4.burn_block_hash, - burnchainBlockHeight: reward4.burn_block_height, - rewards: [reward4], + await mineTenure({ + block_height: 3, + block_hash: '0x33', + index_block_hash: '0x33', + parent_index_block_hash: '0x22', + parent_block_hash: '0x22', + burn_block_hash: '0x1113', + burn_block_height: 202, + tenure_height: 3, }); - // Should return zero rewards since given address was only in blocks that have been reorged into non-canonical. - const rewardQuery1 = await db.getBurnchainRewards({ - burnchainRecipient: addr1, + const rewards = await db.getBurnchainRewards({ limit: 100, offset: 0, }); - expect(rewardQuery1).toEqual([]); - const rewardQuery2 = await db.getBurnchainRewards({ - burnchainRecipient: addr2, + expect(rewards).toHaveLength(6); + expect(rewards.map(r => r.burn_block_hash)).toEqual([ + '0x1113', + '0x1113', + '0x1112', + '0x1112', + '0x1111', + '0x1111', + ]); + + // Create re-org after burn block 201, check rewards again + await mineTenure({ + block_height: 2, + block_hash: '0x22bb', + index_block_hash: '0x22bb', + parent_index_block_hash: '0x11', + parent_block_hash: '0x11', + burn_block_hash: '0x1112bb', + burn_block_height: 201, + tenure_height: 2, + }); + await mineTenure({ + block_height: 3, + block_hash: '0x33bb', + index_block_hash: '0x33bb', + parent_index_block_hash: '0x22bb', + parent_block_hash: '0x22bb', + burn_block_hash: '0x1113bb', + burn_block_height: 202, + tenure_height: 3, + }); + await mineTenure({ + block_height: 4, + block_hash: '0x44bb', + index_block_hash: '0x44bb', + parent_index_block_hash: '0x33bb', + parent_block_hash: '0x33bb', + burn_block_hash: '0x1114', + burn_block_height: 203, + tenure_height: 4, + }); + const rewards2 = await db.getBurnchainRewards({ limit: 100, offset: 0, }); - expect(rewardQuery2).toEqual([ - { - canonical: true, - burn_block_hash: '0x1234', - burn_block_height: 200, - burn_amount: 4001n, - reward_recipient: addr2, - reward_amount: 903n, - reward_index: 0, - }, + expect(rewards2).toHaveLength(8); + expect(rewards2.map(r => r.burn_block_hash)).toEqual([ + '0x1114', + '0x1114', + '0x1113bb', + '0x1113bb', + '0x1112bb', + '0x1112bb', + '0x1111', + '0x1111', ]); }); From 3e46dd75bcb39368e5e7e1a1c7343bcfcd3c9ca1 Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Wed, 8 Oct 2025 14:07:24 -0600 Subject: [PATCH 4/5] fix: coalesce null --- migrations/1759466478081_burnchain-rewards-reorg.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/migrations/1759466478081_burnchain-rewards-reorg.js b/migrations/1759466478081_burnchain-rewards-reorg.js index 253b6b2bf..ce908d1f7 100644 --- a/migrations/1759466478081_burnchain-rewards-reorg.js +++ b/migrations/1759466478081_burnchain-rewards-reorg.js @@ -10,10 +10,13 @@ exports.up = pgm => { ORDER BY burn_block_height DESC, block_height DESC ) UPDATE burnchain_rewards - SET canonical = ( - SELECT canonical - FROM burn_blocks - WHERE burnchain_rewards.burn_block_hash = burn_blocks.burn_block_hash + SET canonical = COALESCE( + ( + SELECT canonical + FROM burn_blocks + WHERE burnchain_rewards.burn_block_hash = burn_blocks.burn_block_hash + ), + false ) `); }; From d99ad5e4454ad733d33cfbad39034ac7888e56fd Mon Sep 17 00:00:00 2001 From: Rafael Cardenas Date: Mon, 20 Oct 2025 10:59:05 -0600 Subject: [PATCH 5/5] fix: simplify migration --- migrations/1759466478081_burnchain-rewards-reorg.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/migrations/1759466478081_burnchain-rewards-reorg.js b/migrations/1759466478081_burnchain-rewards-reorg.js index ce908d1f7..236e0809e 100644 --- a/migrations/1759466478081_burnchain-rewards-reorg.js +++ b/migrations/1759466478081_burnchain-rewards-reorg.js @@ -4,17 +4,13 @@ exports.shorthands = undefined; exports.up = pgm => { pgm.sql(` - WITH burn_blocks AS ( - SELECT DISTINCT ON (burn_block_height) burn_block_hash, canonical - FROM blocks - ORDER BY burn_block_height DESC, block_height DESC - ) UPDATE burnchain_rewards SET canonical = COALESCE( ( SELECT canonical - FROM burn_blocks - WHERE burnchain_rewards.burn_block_hash = burn_blocks.burn_block_hash + FROM blocks + WHERE blocks.burn_block_hash = burnchain_rewards.burn_block_hash + LIMIT 1 ), false )