Skip to content
Open
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
24 changes: 24 additions & 0 deletions migrations/1759466478081_burnchain-rewards-reorg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* 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 = COALESCE(
(
SELECT canonical
FROM burn_blocks
WHERE burnchain_rewards.burn_block_hash = burn_blocks.burn_block_hash
),
false
)
`);
};

exports.down = pgm => {};
34 changes: 6 additions & 28 deletions src/datastore/pg-write-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1923,35 +1923,8 @@ export class PgWriteStore extends PgStore {
};
}

async updateBurnchainRewards({
burnchainBlockHash,
burnchainBlockHeight,
rewards,
}: {
burnchainBlockHash: string;
burnchainBlockHeight: number;
rewards: DbBurnchainReward[];
}): Promise<void> {
async updateBurnchainRewards({ rewards }: { rewards: DbBurnchainReward[] }): Promise<void> {
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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 0 additions & 2 deletions src/event-stream/event-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
18 changes: 0 additions & 18 deletions tests/api/burnchain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
});

Expand Down Expand Up @@ -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(
Expand All @@ -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}`);
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
197 changes: 129 additions & 68 deletions tests/api/datastore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
]);
});

Expand Down