Skip to content

Commit 6f782d5

Browse files
authored
Merge pull request #232 from proto-kit/fix/atomic-block-production
Added recovery mechanism for missing block result
2 parents 676d651 + f6cd8c2 commit 6f782d5

File tree

13 files changed

+177
-85
lines changed

13 files changed

+177
-85
lines changed

packages/persistance/src/services/prisma/PrismaBlockStorage.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BlockStorage,
99
BlockWithResult,
1010
BlockWithPreviousResult,
11+
BlockWithMaybeResult,
1112
} from "@proto-kit/sequencer";
1213
import { filterNonNull, log } from "@proto-kit/common";
1314
import {
@@ -39,7 +40,7 @@ export class PrismaBlockStorage
3940

4041
private async getBlockByQuery(
4142
where: { height: number } | { hash: string }
42-
): Promise<BlockWithResult | undefined> {
43+
): Promise<BlockWithMaybeResult | undefined> {
4344
const dbResult = await this.connection.prismaClient.block.findFirst({
4445
where,
4546
include: {
@@ -57,18 +58,15 @@ export class PrismaBlockStorage
5758
const transactions = dbResult.transactions.map<TransactionExecutionResult>(
5859
(txresult) => this.transactionResultMapper.mapIn([txresult, txresult.tx])
5960
);
60-
if (dbResult.result === undefined || dbResult.result === null) {
61-
throw new Error(
62-
`No Metadata has been set for block ${JSON.stringify(where)} yet`
63-
);
64-
}
6561

6662
return {
6763
block: {
6864
...this.blockMapper.mapIn(dbResult),
6965
transactions,
7066
},
71-
result: this.blockResultMapper.mapIn(dbResult.result),
67+
result: dbResult.result
68+
? this.blockResultMapper.mapIn(dbResult.result)
69+
: undefined,
7270
};
7371
}
7472

@@ -169,7 +167,9 @@ export class PrismaBlockStorage
169167
return (result?._max.height ?? -1) + 1;
170168
}
171169

172-
public async getLatestBlock(): Promise<BlockWithResult | undefined> {
170+
public async getLatestBlockAndResult(): Promise<
171+
BlockWithMaybeResult | undefined
172+
> {
173173
const latestBlock = await this.connection.prismaClient.$queryRaw<
174174
{ hash: string }[]
175175
>`SELECT b1."hash" FROM "Block" b1
@@ -185,6 +185,22 @@ export class PrismaBlockStorage
185185
});
186186
}
187187

188+
public async getLatestBlock(): Promise<BlockWithResult | undefined> {
189+
const result = await this.getLatestBlockAndResult();
190+
if (result !== undefined) {
191+
if (result.result === undefined) {
192+
throw new Error(
193+
`Block result for block ${result.block.height.toString()} not found`
194+
);
195+
}
196+
return {
197+
block: result.block,
198+
result: result.result,
199+
};
200+
}
201+
return result;
202+
}
203+
188204
public async getNewBlocks(): Promise<BlockWithPreviousResult[]> {
189205
const blocks = await this.connection.prismaClient.block.findMany({
190206
where: {

packages/sdk/src/query/BlockStorageNetworkStateModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class BlockStorageNetworkStateModule
5151
* with afterBundle() hooks executed
5252
*/
5353
public async getStagedNetworkState(): Promise<NetworkState | undefined> {
54-
const result = await this.unprovenQueue.getLatestBlock();
54+
const result = await this.unprovenStorage.getLatestBlock();
5555
return result?.result.afterNetworkState;
5656
}
5757

packages/sequencer/src/protocol/production/sequencing/BlockProducerModule.ts

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { inject } from "tsyringe";
2-
import { log, noop } from "@proto-kit/common";
2+
import { log } from "@proto-kit/common";
33
import { ACTIONS_EMPTY_HASH } from "@proto-kit/protocol";
44
import {
55
MethodIdResolver,
@@ -18,7 +18,11 @@ import { BlockQueue } from "../../../storage/repositories/BlockStorage";
1818
import { PendingTransaction } from "../../../mempool/PendingTransaction";
1919
import { AsyncMerkleTreeStore } from "../../../state/async/AsyncMerkleTreeStore";
2020
import { AsyncStateService } from "../../../state/async/AsyncStateService";
21-
import { Block, BlockWithResult } from "../../../storage/model/Block";
21+
import {
22+
Block,
23+
BlockResult,
24+
BlockWithResult,
25+
} from "../../../storage/model/Block";
2226
import { CachedStateService } from "../../../state/state/CachedStateService";
2327
import { MessageStorage } from "../../../storage/repositories/MessageStorage";
2428

@@ -99,7 +103,23 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
99103
}
100104
}
101105

102-
public async tryProduceBlock(): Promise<BlockWithResult | undefined> {
106+
public async generateMetadata(block: Block): Promise<BlockResult> {
107+
const { result, blockHashTreeStore, treeStore } =
108+
await this.executionService.generateMetadataForNextBlock(
109+
block,
110+
this.unprovenMerkleStore,
111+
this.blockTreeStore
112+
);
113+
114+
await blockHashTreeStore.mergeIntoParent();
115+
await treeStore.mergeIntoParent();
116+
117+
await this.blockQueue.pushResult(result);
118+
119+
return result;
120+
}
121+
122+
public async tryProduceBlock(): Promise<Block | undefined> {
103123
if (!this.productionInProgress) {
104124
try {
105125
const block = await this.produceBlock();
@@ -118,20 +138,7 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
118138
);
119139
this.prettyPrintBlockContents(block);
120140

121-
// Generate metadata for next block
122-
123-
// TODO: make async of production in the future
124-
const result = await this.executionService.generateMetadataForNextBlock(
125-
block,
126-
this.unprovenMerkleStore,
127-
this.blockTreeStore,
128-
true
129-
);
130-
131-
return {
132-
block,
133-
result,
134-
};
141+
return block;
135142
} catch (error: unknown) {
136143
if (error instanceof Error) {
137144
throw error;
@@ -151,19 +158,31 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
151158
}> {
152159
const txs = await this.mempool.getTxs(this.maximumBlockSize());
153160

154-
const parentBlock = await this.blockQueue.getLatestBlock();
161+
const parentBlock = await this.blockQueue.getLatestBlockAndResult();
162+
163+
let metadata: BlockWithResult;
155164

156165
if (parentBlock === undefined) {
157166
log.debug(
158167
"No block metadata given, assuming first block, generating genesis metadata"
159168
);
169+
metadata = BlockWithResult.createEmpty();
170+
} else if (parentBlock.result === undefined) {
171+
throw new Error(
172+
`Metadata for block at height ${parentBlock.block.height.toString()} not available`
173+
);
174+
} else {
175+
metadata = {
176+
block: parentBlock.block,
177+
// By reconstructing this object, typescript correctly infers the result to be defined
178+
result: parentBlock.result,
179+
};
160180
}
161181

162182
const messages = await this.messageStorage.getMessages(
163183
parentBlock?.block.toMessagesHash.toString() ??
164184
ACTIONS_EMPTY_HASH.toString()
165185
);
166-
const metadata = parentBlock ?? BlockWithResult.createEmpty();
167186

168187
log.debug(
169188
`Block collected, ${txs.length} txs, ${messages.length} messages`
@@ -196,14 +215,29 @@ export class BlockProducerModule extends SequencerModule<BlockConfig> {
196215
this.allowEmptyBlock()
197216
);
198217

199-
await cachedStateService.mergeIntoParent();
218+
if (block !== undefined) {
219+
await cachedStateService.mergeIntoParent();
220+
221+
await this.blockQueue.pushBlock(block);
222+
}
200223

201224
this.productionInProgress = false;
202225

203226
return block;
204227
}
205228

206229
public async start() {
207-
noop();
230+
// Check if metadata height is behind block production.
231+
// This can happen when the sequencer crashes after a block has been produced
232+
// but before the metadata generation has finished
233+
const latestBlock = await this.blockQueue.getLatestBlockAndResult();
234+
// eslint-disable-next-line sonarjs/no-collapsible-if
235+
if (latestBlock !== undefined) {
236+
if (latestBlock.result === undefined) {
237+
await this.generateMetadata(latestBlock.block);
238+
}
239+
// Here, the metadata has been computed already
240+
}
241+
// If we reach here, its a genesis startup, no blocks exist yet
208242
}
209243
}

packages/sequencer/src/protocol/production/sequencing/TransactionExecutionService.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,12 @@ export class TransactionExecutionService {
323323
public async generateMetadataForNextBlock(
324324
block: Block,
325325
merkleTreeStore: AsyncMerkleTreeStore,
326-
blockHashTreeStore: AsyncMerkleTreeStore,
327-
modifyTreeStore = true
328-
): Promise<BlockResult> {
326+
blockHashTreeStore: AsyncMerkleTreeStore
327+
): Promise<{
328+
result: BlockResult;
329+
treeStore: CachedMerkleTreeStore;
330+
blockHashTreeStore: CachedMerkleTreeStore;
331+
}> {
329332
// Flatten diff list into a single diff by applying them over each other
330333
const combinedDiff = block.transactions
331334
.map((tx) => {
@@ -403,22 +406,21 @@ export class TransactionExecutionService {
403406
);
404407
const blockHashWitness = blockHashTree.getWitness(block.height.toBigInt());
405408
const newBlockHashRoot = blockHashTree.getRoot();
406-
await blockHashInMemoryStore.mergeIntoParent();
407-
408-
if (modifyTreeStore) {
409-
await inMemoryStore.mergeIntoParent();
410-
}
411409

412410
return {
413-
afterNetworkState: resultingNetworkState,
414-
stateRoot: stateRoot.toBigInt(),
415-
blockHashRoot: newBlockHashRoot.toBigInt(),
416-
blockHashWitness,
417-
418-
blockStateTransitions: reducedStateTransitions.map((st) =>
419-
UntypedStateTransition.fromStateTransition(st)
420-
),
421-
blockHash: block.hash.toBigInt(),
411+
result: {
412+
afterNetworkState: resultingNetworkState,
413+
stateRoot: stateRoot.toBigInt(),
414+
blockHashRoot: newBlockHashRoot.toBigInt(),
415+
blockHashWitness,
416+
417+
blockStateTransitions: reducedStateTransitions.map((st) =>
418+
UntypedStateTransition.fromStateTransition(st)
419+
),
420+
blockHash: block.hash.toBigInt(),
421+
},
422+
treeStore: inMemoryStore,
423+
blockHashTreeStore: blockHashInMemoryStore,
422424
};
423425
}
424426

packages/sequencer/src/protocol/production/trigger/BlockTrigger.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,29 @@ export class BlockTriggerBase<
6565
return undefined;
6666
}
6767

68-
protected async produceBlockWithResult(
69-
enqueueInSettlementQueue: boolean
70-
): Promise<BlockWithResult | undefined> {
68+
protected async produceBlockWithResult(): Promise<
69+
BlockWithResult | undefined
70+
> {
7171
const block = await this.blockProducerModule.tryProduceBlock();
72+
if (block) {
73+
this.events.emit("block-produced", block);
7274

73-
if (block && enqueueInSettlementQueue) {
74-
await this.blockQueue.pushBlock(block.block);
75-
this.events.emit("block-produced", block.block);
75+
const result = await this.blockProducerModule.generateMetadata(block);
7676

77-
await this.blockQueue.pushResult(block.result);
78-
this.events.emit("block-metadata-produced", block);
79-
}
77+
const blockWithMetadata = {
78+
block,
79+
result,
80+
};
81+
82+
this.events.emit("block-metadata-produced", blockWithMetadata);
8083

81-
return block;
84+
return blockWithMetadata;
85+
}
86+
return undefined;
8287
}
8388

84-
protected async produceBlock(
85-
enqueueInSettlementQueue: boolean
86-
): Promise<Block | undefined> {
87-
const blockWithResult = await this.produceBlockWithResult(
88-
enqueueInSettlementQueue
89-
);
89+
protected async produceBlock(): Promise<Block | undefined> {
90+
const blockWithResult = await this.produceBlockWithResult();
9091

9192
return blockWithResult?.block;
9293
}

packages/sequencer/src/protocol/production/trigger/ManualBlockTrigger.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,11 @@ export class ManualBlockTrigger
6262
return await super.settle(batch);
6363
}
6464

65-
public async produceBlock(
66-
enqueueInSettlementQueue: boolean = true
67-
): Promise<Block | undefined> {
68-
return await super.produceBlock(enqueueInSettlementQueue);
65+
public async produceBlock(): Promise<Block | undefined> {
66+
return await super.produceBlock();
6967
}
7068

71-
public async produceBlockWithResult(
72-
enqueueInSettlementQueue: boolean = true
73-
): Promise<BlockWithResult | undefined> {
74-
return await super.produceBlockWithResult(enqueueInSettlementQueue);
69+
public async produceBlockWithResult(): Promise<BlockWithResult | undefined> {
70+
return await super.produceBlockWithResult();
7571
}
7672
}

packages/sequencer/src/protocol/production/trigger/TimedBlockTrigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export class TimedBlockTrigger
129129
// Produce a block if either produceEmptyBlocks is true or we have more
130130
// than 1 tx in mempool
131131
if (mempoolTxs.length > 0 || (this.config.produceEmptyBlocks ?? true)) {
132-
await this.produceBlock(true);
132+
await this.produceBlock();
133133
}
134134
}
135135

packages/sequencer/src/storage/inmemory/InMemoryBlockStorage.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
BlockQueue,
66
BlockStorage,
77
} from "../repositories/BlockStorage";
8-
import type { Block, BlockResult, BlockWithResult } from "../model/Block";
8+
import type {
9+
Block,
10+
BlockResult,
11+
BlockWithMaybeResult,
12+
BlockWithResult,
13+
} from "../model/Block";
914
import { BlockWithPreviousResult } from "../../protocol/production/BatchProducerModule";
1015
import { BatchStorage } from "../repositories/BatchStorage";
1116

@@ -29,10 +34,12 @@ export class InMemoryBlockStorage
2934
return this.blocks.length;
3035
}
3136

32-
public async getLatestBlock(): Promise<BlockWithResult | undefined> {
37+
public async getLatestBlockAndResult(): Promise<
38+
BlockWithMaybeResult | undefined
39+
> {
3340
const currentHeight = await this.getCurrentBlockHeight();
3441
const block = await this.getBlockAt(currentHeight - 1);
35-
const result = this.results[currentHeight - 1];
42+
const result: BlockResult | undefined = this.results[currentHeight - 1];
3643
if (block === undefined) {
3744
return undefined;
3845
}
@@ -42,6 +49,22 @@ export class InMemoryBlockStorage
4249
};
4350
}
4451

52+
public async getLatestBlock(): Promise<BlockWithResult | undefined> {
53+
const result = await this.getLatestBlockAndResult();
54+
if (result !== undefined) {
55+
if (result.result === undefined) {
56+
throw new Error(
57+
`Block result for block ${result.block.height.toString()} not found`
58+
);
59+
}
60+
return {
61+
block: result.block,
62+
result: result.result,
63+
};
64+
}
65+
return result;
66+
}
67+
4568
public async getNewBlocks(): Promise<BlockWithPreviousResult[]> {
4669
const latestBatch = await this.batchStorage.getLatestBatch();
4770

0 commit comments

Comments
 (0)