diff --git a/migrations/1759466478081_burnchain-rewards-reorg.js b/migrations/1759466478081_burnchain-rewards-reorg.js new file mode 100644 index 000000000..236e0809e --- /dev/null +++ b/migrations/1759466478081_burnchain-rewards-reorg.js @@ -0,0 +1,20 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = pgm => { + pgm.sql(` + UPDATE burnchain_rewards + SET canonical = COALESCE( + ( + SELECT canonical + FROM blocks + WHERE blocks.burn_block_hash = burnchain_rewards.burn_block_hash + LIMIT 1 + ), + false + ) + `); +}; + +exports.down = pgm => {}; 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({ 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', ]); });