Skip to content

Commit f82efbc

Browse files
authored
fix: calculate burnchain reward reorgs correctly (#2355)
* fix: reorg burnchain rewards * fix: migration * fix: tests * fix: coalesce null * fix: simplify migration
1 parent 5c49f49 commit f82efbc

File tree

5 files changed

+155
-116
lines changed

5 files changed

+155
-116
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable camelcase */
2+
3+
exports.shorthands = undefined;
4+
5+
exports.up = pgm => {
6+
pgm.sql(`
7+
UPDATE burnchain_rewards
8+
SET canonical = COALESCE(
9+
(
10+
SELECT canonical
11+
FROM blocks
12+
WHERE blocks.burn_block_hash = burnchain_rewards.burn_block_hash
13+
LIMIT 1
14+
),
15+
false
16+
)
17+
`);
18+
};
19+
20+
exports.down = pgm => {};

src/datastore/pg-write-store.ts

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1923,35 +1923,8 @@ export class PgWriteStore extends PgStore {
19231923
};
19241924
}
19251925

1926-
async updateBurnchainRewards({
1927-
burnchainBlockHash,
1928-
burnchainBlockHeight,
1929-
rewards,
1930-
}: {
1931-
burnchainBlockHash: string;
1932-
burnchainBlockHeight: number;
1933-
rewards: DbBurnchainReward[];
1934-
}): Promise<void> {
1926+
async updateBurnchainRewards({ rewards }: { rewards: DbBurnchainReward[] }): Promise<void> {
19351927
return await this.sqlWriteTransaction(async sql => {
1936-
const existingRewards = await sql<
1937-
{
1938-
reward_recipient: string;
1939-
reward_amount: string;
1940-
}[]
1941-
>`
1942-
UPDATE burnchain_rewards
1943-
SET canonical = false
1944-
WHERE canonical = true AND
1945-
(burn_block_hash = ${burnchainBlockHash}
1946-
OR burn_block_height >= ${burnchainBlockHeight})
1947-
`;
1948-
1949-
if (existingRewards.count > 0) {
1950-
logger.warn(
1951-
`Invalidated ${existingRewards.count} burnchain rewards after fork detected at burnchain block ${burnchainBlockHash}`
1952-
);
1953-
}
1954-
19551928
for (const reward of rewards) {
19561929
const values: BurnchainRewardInsertValues = {
19571930
canonical: true,
@@ -3607,6 +3580,11 @@ export class PgWriteStore extends PgStore {
36073580
if (orphanedBlockResult.length > 0) {
36083581
const orphanedBlocks = orphanedBlockResult.map(b => parseBlockQueryResult(b));
36093582
for (const orphanedBlock of orphanedBlocks) {
3583+
await sql`
3584+
UPDATE burnchain_rewards
3585+
SET canonical = false
3586+
WHERE canonical = true AND burn_block_hash = ${orphanedBlock.burn_block_hash}
3587+
`;
36103588
const microCanonicalUpdateResult = await this.updateMicroCanonical(sql, {
36113589
isCanonical: false,
36123590
blockHeight: orphanedBlock.block_height,

src/event-stream/event-server.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ async function handleBurnBlockMessage(
121121
return slotHolder;
122122
});
123123
await db.updateBurnchainRewards({
124-
burnchainBlockHash: burnBlockMsg.burn_block_hash,
125-
burnchainBlockHeight: burnBlockMsg.burn_block_height,
126124
rewards: rewards,
127125
});
128126
await db.updateBurnchainRewardSlotHolders({

tests/api/burnchain.test.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,9 @@ describe('burnchain tests', () => {
201201
reward_index: 2,
202202
};
203203
await db.updateBurnchainRewards({
204-
burnchainBlockHash: reward1.burn_block_hash,
205-
burnchainBlockHeight: reward1.burn_block_height,
206204
rewards: [reward1, reward2],
207205
});
208206
await db.updateBurnchainRewards({
209-
burnchainBlockHash: reward3.burn_block_hash,
210-
burnchainBlockHeight: reward3.burn_block_height,
211207
rewards: [reward3, reward4, reward5],
212208
});
213209

@@ -282,18 +278,12 @@ describe('burnchain tests', () => {
282278
reward_index: 0,
283279
};
284280
await db.updateBurnchainRewards({
285-
burnchainBlockHash: reward1.burn_block_hash,
286-
burnchainBlockHeight: reward1.burn_block_height,
287281
rewards: [reward1],
288282
});
289283
await db.updateBurnchainRewards({
290-
burnchainBlockHash: reward2.burn_block_hash,
291-
burnchainBlockHeight: reward2.burn_block_height,
292284
rewards: [reward2],
293285
});
294286
await db.updateBurnchainRewards({
295-
burnchainBlockHash: reward3.burn_block_hash,
296-
burnchainBlockHeight: reward3.burn_block_height,
297287
rewards: [reward3],
298288
});
299289
const rewardResult = await supertest(api.server).get(
@@ -320,8 +310,6 @@ describe('burnchain tests', () => {
320310
reward_index: 0,
321311
};
322312
await db.updateBurnchainRewards({
323-
burnchainBlockHash: reward1.burn_block_hash,
324-
burnchainBlockHeight: reward1.burn_block_height,
325313
rewards: [reward1],
326314
});
327315
const rewardResult = await supertest(api.server).get(`/extended/v1/burnchain/rewards/${addr1}`);
@@ -360,8 +348,6 @@ describe('burnchain tests', () => {
360348
reward_index: 0,
361349
};
362350
await db.updateBurnchainRewards({
363-
burnchainBlockHash: reward1.burn_block_hash,
364-
burnchainBlockHeight: reward1.burn_block_height,
365351
rewards: [reward1],
366352
});
367353
const rewardResult = await supertest(api.server).get(
@@ -402,8 +388,6 @@ describe('burnchain tests', () => {
402388
reward_index: 0,
403389
};
404390
await db.updateBurnchainRewards({
405-
burnchainBlockHash: reward1.burn_block_hash,
406-
burnchainBlockHeight: reward1.burn_block_height,
407391
rewards: [reward1],
408392
});
409393
const rewardResult = await supertest(api.server).get(
@@ -444,8 +428,6 @@ describe('burnchain tests', () => {
444428
reward_index: 0,
445429
};
446430
await db.updateBurnchainRewards({
447-
burnchainBlockHash: reward1.burn_block_hash,
448-
burnchainBlockHeight: reward1.burn_block_height,
449431
rewards: [reward1],
450432
});
451433
const rewardResult = await supertest(api.server).get(

tests/api/datastore.test.ts

Lines changed: 129 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3043,13 +3043,9 @@ describe('postgres datastore', () => {
30433043
reward_index: 0,
30443044
};
30453045
await db.updateBurnchainRewards({
3046-
burnchainBlockHash: reward1.burn_block_hash,
3047-
burnchainBlockHeight: reward1.burn_block_height,
30483046
rewards: [reward1, reward2],
30493047
});
30503048
await db.updateBurnchainRewards({
3051-
burnchainBlockHash: reward3.burn_block_hash,
3052-
burnchainBlockHeight: reward3.burn_block_height,
30533049
rewards: [reward3],
30543050
});
30553051
const rewardQuery = await db.getBurnchainRewards({
@@ -3091,80 +3087,145 @@ describe('postgres datastore', () => {
30913087
test('pg burnchain reward reorg handling', async () => {
30923088
const addr1 = '1G4ayBXJvxZMoZpaNdZG6VyWwWq2mHpMjQ';
30933089
const addr2 = '1DDUAqoyXvhF4cxznN9uL6j9ok1oncsT2z';
3094-
const reward1: DbBurnchainReward = {
3095-
canonical: true,
3096-
burn_block_hash: '0x1234',
3097-
burn_block_height: 200,
3098-
burn_amount: 2000n,
3099-
reward_recipient: addr1,
3100-
reward_amount: 900n,
3101-
reward_index: 0,
3102-
};
3103-
const reward2: DbBurnchainReward = {
3104-
canonical: true,
3105-
burn_block_hash: '0x1234',
3090+
3091+
const mineTenure = async (args: {
3092+
block_height: number;
3093+
block_hash: string;
3094+
index_block_hash: string;
3095+
parent_index_block_hash: string;
3096+
parent_block_hash: string;
3097+
burn_block_hash: string;
3098+
burn_block_height: number;
3099+
tenure_height: number;
3100+
}) => {
3101+
// Add some rewards
3102+
await db.updateBurnchainRewards({
3103+
rewards: [
3104+
{
3105+
canonical: true,
3106+
burn_block_hash: args.burn_block_hash,
3107+
burn_block_height: args.burn_block_height,
3108+
burn_amount: 2000n,
3109+
reward_recipient: addr1,
3110+
reward_amount: 900n,
3111+
reward_index: 0,
3112+
},
3113+
{
3114+
canonical: true,
3115+
burn_block_hash: args.burn_block_hash,
3116+
burn_block_height: args.burn_block_height,
3117+
burn_amount: 2001n,
3118+
reward_recipient: addr2,
3119+
reward_amount: 901n,
3120+
reward_index: 1,
3121+
},
3122+
],
3123+
});
3124+
// Mine a tenure based on that burn block
3125+
await db.update(
3126+
new TestBlockBuilder({
3127+
block_height: args.block_height,
3128+
block_hash: args.block_hash,
3129+
index_block_hash: args.index_block_hash,
3130+
parent_index_block_hash: args.parent_index_block_hash,
3131+
parent_block_hash: args.parent_block_hash,
3132+
burn_block_hash: args.burn_block_hash,
3133+
burn_block_height: args.burn_block_height,
3134+
tenure_height: args.tenure_height,
3135+
}).build()
3136+
);
3137+
};
3138+
3139+
// Mine 3 tenures, check rewards
3140+
await mineTenure({
3141+
block_height: 1,
3142+
block_hash: '0x11',
3143+
index_block_hash: '0x11',
3144+
parent_index_block_hash: '0x0000',
3145+
parent_block_hash: '0x0000',
3146+
burn_block_hash: '0x1111',
31063147
burn_block_height: 200,
3107-
burn_amount: 2001n,
3108-
reward_recipient: addr1,
3109-
reward_amount: 901n,
3110-
reward_index: 1,
3111-
};
3112-
const reward3: DbBurnchainReward = {
3113-
canonical: true,
3114-
burn_block_hash: '0x2345',
3115-
burn_block_height: 201,
3116-
burn_amount: 3001n,
3117-
reward_recipient: addr1,
3118-
reward_amount: 902n,
3119-
reward_index: 0,
3120-
};
3121-
// block that triggers a reorg of all previous
3122-
const reward4: DbBurnchainReward = {
3123-
canonical: true,
3124-
burn_block_hash: reward1.burn_block_hash,
3125-
burn_block_height: reward1.burn_block_height,
3126-
burn_amount: 4001n,
3127-
reward_recipient: addr2,
3128-
reward_amount: 903n,
3129-
reward_index: 0,
3130-
};
3131-
await db.updateBurnchainRewards({
3132-
burnchainBlockHash: reward1.burn_block_hash,
3133-
burnchainBlockHeight: reward1.burn_block_height,
3134-
rewards: [reward1, reward2],
3148+
tenure_height: 1,
31353149
});
3136-
await db.updateBurnchainRewards({
3137-
burnchainBlockHash: reward3.burn_block_hash,
3138-
burnchainBlockHeight: reward3.burn_block_height,
3139-
rewards: [reward3],
3150+
await mineTenure({
3151+
block_height: 2,
3152+
block_hash: '0x22',
3153+
index_block_hash: '0x22',
3154+
parent_index_block_hash: '0x11',
3155+
parent_block_hash: '0x11',
3156+
burn_block_hash: '0x1112',
3157+
burn_block_height: 201,
3158+
tenure_height: 2,
31403159
});
3141-
await db.updateBurnchainRewards({
3142-
burnchainBlockHash: reward4.burn_block_hash,
3143-
burnchainBlockHeight: reward4.burn_block_height,
3144-
rewards: [reward4],
3160+
await mineTenure({
3161+
block_height: 3,
3162+
block_hash: '0x33',
3163+
index_block_hash: '0x33',
3164+
parent_index_block_hash: '0x22',
3165+
parent_block_hash: '0x22',
3166+
burn_block_hash: '0x1113',
3167+
burn_block_height: 202,
3168+
tenure_height: 3,
31453169
});
3146-
// Should return zero rewards since given address was only in blocks that have been reorged into non-canonical.
3147-
const rewardQuery1 = await db.getBurnchainRewards({
3148-
burnchainRecipient: addr1,
3170+
const rewards = await db.getBurnchainRewards({
31493171
limit: 100,
31503172
offset: 0,
31513173
});
3152-
expect(rewardQuery1).toEqual([]);
3153-
const rewardQuery2 = await db.getBurnchainRewards({
3154-
burnchainRecipient: addr2,
3174+
expect(rewards).toHaveLength(6);
3175+
expect(rewards.map(r => r.burn_block_hash)).toEqual([
3176+
'0x1113',
3177+
'0x1113',
3178+
'0x1112',
3179+
'0x1112',
3180+
'0x1111',
3181+
'0x1111',
3182+
]);
3183+
3184+
// Create re-org after burn block 201, check rewards again
3185+
await mineTenure({
3186+
block_height: 2,
3187+
block_hash: '0x22bb',
3188+
index_block_hash: '0x22bb',
3189+
parent_index_block_hash: '0x11',
3190+
parent_block_hash: '0x11',
3191+
burn_block_hash: '0x1112bb',
3192+
burn_block_height: 201,
3193+
tenure_height: 2,
3194+
});
3195+
await mineTenure({
3196+
block_height: 3,
3197+
block_hash: '0x33bb',
3198+
index_block_hash: '0x33bb',
3199+
parent_index_block_hash: '0x22bb',
3200+
parent_block_hash: '0x22bb',
3201+
burn_block_hash: '0x1113bb',
3202+
burn_block_height: 202,
3203+
tenure_height: 3,
3204+
});
3205+
await mineTenure({
3206+
block_height: 4,
3207+
block_hash: '0x44bb',
3208+
index_block_hash: '0x44bb',
3209+
parent_index_block_hash: '0x33bb',
3210+
parent_block_hash: '0x33bb',
3211+
burn_block_hash: '0x1114',
3212+
burn_block_height: 203,
3213+
tenure_height: 4,
3214+
});
3215+
const rewards2 = await db.getBurnchainRewards({
31553216
limit: 100,
31563217
offset: 0,
31573218
});
3158-
expect(rewardQuery2).toEqual([
3159-
{
3160-
canonical: true,
3161-
burn_block_hash: '0x1234',
3162-
burn_block_height: 200,
3163-
burn_amount: 4001n,
3164-
reward_recipient: addr2,
3165-
reward_amount: 903n,
3166-
reward_index: 0,
3167-
},
3219+
expect(rewards2).toHaveLength(8);
3220+
expect(rewards2.map(r => r.burn_block_hash)).toEqual([
3221+
'0x1114',
3222+
'0x1114',
3223+
'0x1113bb',
3224+
'0x1113bb',
3225+
'0x1112bb',
3226+
'0x1112bb',
3227+
'0x1111',
3228+
'0x1111',
31683229
]);
31693230
});
31703231

0 commit comments

Comments
 (0)