Skip to content

Commit 41afd2e

Browse files
authored
Merge pull request #229 from proto-kit/fix/block-merging
Fix bug in protocol and flow for merging blocks
2 parents 8167e97 + e9e6a97 commit 41afd2e

File tree

6 files changed

+73
-33
lines changed

6 files changed

+73
-33
lines changed

packages/common/src/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,5 @@ type NonMethodKeys<Type> = {
147147
[Key in keyof Type]: Type[Key] extends Function ? never : Key;
148148
}[keyof Type];
149149
export type NonMethods<Type> = Pick<Type, NonMethodKeys<Type>>;
150+
151+
export const MAX_FIELD = Field(Field.ORDER - 1n);

packages/protocol/src/prover/block/BlockProvable.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class BlockProverPublicInput extends Struct({
2424
blockHashRoot: Field,
2525
eternalTransactionsHash: Field,
2626
incomingMessagesHash: Field,
27+
blockNumber: Field,
2728
}) {}
2829

2930
export class BlockProverPublicOutput extends Struct({
@@ -36,15 +37,10 @@ export class BlockProverPublicOutput extends Struct({
3637
closed: Bool,
3738
blockNumber: Field,
3839
}) {
39-
public equals(
40-
input: BlockProverPublicInput,
41-
closed: Bool,
42-
blockNumber: Field
43-
): Bool {
40+
public equals(input: BlockProverPublicInput, closed: Bool): Bool {
4441
const output2 = BlockProverPublicOutput.toFields({
4542
...input,
4643
closed,
47-
blockNumber,
4844
});
4945
const output1 = BlockProverPublicOutput.toFields(this);
5046
return output1

packages/protocol/src/prover/block/BlockProver.ts

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { container, inject, injectable, injectAll } from "tsyringe";
1313
import {
1414
AreProofsEnabled,
15+
MAX_FIELD,
1516
PlainZkProgram,
1617
provableMethod,
1718
WithZkProgrammable,
@@ -124,10 +125,6 @@ export interface BlockProverState {
124125
incomingMessagesHash: Field;
125126
}
126127

127-
function maxField() {
128-
return Field(Field.ORDER - 1n);
129-
}
130-
131128
export type BlockProof = Proof<BlockProverPublicInput, BlockProverPublicOutput>;
132129
export type RuntimeProof = Proof<void, MethodPublicOutput>;
133130

@@ -416,6 +413,11 @@ export class BlockProverProgrammable extends ZkProgrammable<
416413
"ExecutionData Networkstate doesn't equal public input hash"
417414
);
418415

416+
publicInput.blockNumber.assertEquals(
417+
MAX_FIELD,
418+
"blockNumber has to be MAX for transaction proofs"
419+
);
420+
419421
// Verify the [methodId, vk] tuple against the baked-in vk tree root
420422
const { verificationKey, witness: verificationKeyTreeWitness } =
421423
verificationKeyWitness;
@@ -445,7 +447,7 @@ export class BlockProverProgrammable extends ZkProgrammable<
445447

446448
return new BlockProverPublicOutput({
447449
...stateTo,
448-
blockNumber: maxField(),
450+
blockNumber: publicInput.blockNumber,
449451
closed: Bool(false),
450452
});
451453
}
@@ -525,16 +527,16 @@ export class BlockProverProgrammable extends ZkProgrammable<
525527
.not();
526528
stateTransitionProof.verifyIf(stsEmitted);
527529

528-
// Verify Transaction proof if it has at least 1 tx
530+
// Verify Transaction proof if it has at least 1 tx - i.e. the
531+
// input and output doesn't match fully
529532
// We have to compare the whole input and output because we can make no
530533
// assumptions about the values, since it can be an arbitrary dummy-proof
531534
const txProofOutput = transactionProof.publicOutput;
532535
const verifyTransactionProof = txProofOutput.equals(
533536
transactionProof.publicInput,
534-
txProofOutput.closed,
535-
txProofOutput.blockNumber
537+
txProofOutput.closed
536538
);
537-
transactionProof.verifyIf(verifyTransactionProof);
539+
transactionProof.verifyIf(verifyTransactionProof.not());
538540

539541
// 2. Execute beforeBlock hooks
540542
const beforeBlockResult = await this.executeBlockHooks(
@@ -616,6 +618,8 @@ export class BlockProverProgrammable extends ZkProgrammable<
616618
// Calculate the new block index
617619
const blockIndex = blockWitness.calculateIndex();
618620

621+
blockIndex.assertEquals(publicInput.blockNumber);
622+
619623
blockWitness
620624
.calculateRoot(Field(0))
621625
.assertEquals(
@@ -633,7 +637,7 @@ export class BlockProverProgrammable extends ZkProgrammable<
633637

634638
return new BlockProverPublicOutput({
635639
...state,
636-
blockNumber: blockIndex,
640+
blockNumber: blockIndex.add(1),
637641
closed: Bool(true),
638642
});
639643
}
@@ -740,19 +744,25 @@ export class BlockProverProgrammable extends ZkProgrammable<
740744
// assert proof1.height == proof2.height
741745
// }
742746

743-
const proof1Height = proof1.publicOutput.blockNumber;
744747
const proof1Closed = proof1.publicOutput.closed;
745-
const proof2Height = proof2.publicOutput.blockNumber;
746748
const proof2Closed = proof2.publicOutput.closed;
747749

748-
const isValidTransactionMerge = proof1Height
749-
.equals(maxField())
750-
.and(proof2Height.equals(proof1Height))
750+
const blockNumberProgressionValid = publicInput.blockNumber
751+
.equals(proof1.publicInput.blockNumber)
752+
.and(
753+
proof1.publicOutput.blockNumber.equals(proof2.publicInput.blockNumber)
754+
);
755+
756+
// For tx proofs, we check that the progression starts and end with MAX
757+
// in addition to that both proofs are non-closed
758+
const isValidTransactionMerge = publicInput.blockNumber
759+
.equals(MAX_FIELD)
760+
.and(blockNumberProgressionValid)
751761
.and(proof1Closed.or(proof2Closed).not());
752762

753763
const isValidClosedMerge = proof1Closed
754764
.and(proof2Closed)
755-
.and(proof1Height.add(1).equals(proof2Height));
765+
.and(blockNumberProgressionValid);
756766

757767
isValidTransactionMerge
758768
.or(isValidClosedMerge)
@@ -765,9 +775,8 @@ export class BlockProverProgrammable extends ZkProgrammable<
765775
blockHashRoot: proof2.publicOutput.blockHashRoot,
766776
eternalTransactionsHash: proof2.publicOutput.eternalTransactionsHash,
767777
incomingMessagesHash: proof2.publicOutput.incomingMessagesHash,
768-
// Provable.if(isValidClosedMerge, Bool(true), Bool(false));
769778
closed: isValidClosedMerge,
770-
blockNumber: proof2Height,
779+
blockNumber: proof2.publicOutput.blockNumber,
771780
});
772781
}
773782

packages/sequencer/src/protocol/production/BlockTaskFlowService.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Protocol,
1010
StateTransitionProof,
1111
} from "@proto-kit/protocol";
12-
import { log, MOCK_PROOF } from "@proto-kit/common";
12+
import { log, MAX_FIELD, MOCK_PROOF } from "@proto-kit/common";
1313

1414
import { TaskQueue } from "../../worker/queue/TaskQueue";
1515
import { Flow, FlowCreator } from "../../worker/flow/Flow";
@@ -171,9 +171,9 @@ export class BlockTaskFlowService {
171171
mappingTask: this.blockProvingTask,
172172
reductionTask: this.blockReductionTask,
173173

174-
mergableFunction: (a, b) =>
174+
mergableFunction: (a, b) => {
175175
// TODO Proper replication of merge logic
176-
a.publicOutput.stateRoot
176+
const part1 = a.publicOutput.stateRoot
177177
.equals(b.publicInput.stateRoot)
178178
.and(
179179
a.publicOutput.blockHashRoot.equals(b.publicInput.blockHashRoot)
@@ -189,7 +189,28 @@ export class BlockTaskFlowService {
189189
)
190190
)
191191
.and(a.publicOutput.closed.equals(b.publicOutput.closed))
192-
.toBoolean(),
192+
.toBoolean();
193+
194+
const proof1Closed = a.publicOutput.closed;
195+
const proof2Closed = b.publicOutput.closed;
196+
197+
const blockNumberProgressionValid = a.publicOutput.blockNumber.equals(
198+
b.publicInput.blockNumber
199+
);
200+
201+
const isValidTransactionMerge = a.publicInput.blockNumber
202+
.equals(MAX_FIELD)
203+
.and(blockNumberProgressionValid)
204+
.and(proof1Closed.or(proof2Closed).not());
205+
206+
const isValidClosedMerge = proof1Closed
207+
.and(proof2Closed)
208+
.and(blockNumberProgressionValid);
209+
210+
return (
211+
part1 && isValidClosedMerge.or(isValidTransactionMerge).toBoolean()
212+
);
213+
},
193214
},
194215
this.flowCreator
195216
);
@@ -286,13 +307,13 @@ export class BlockTaskFlowService {
286307
blockTrace.block.publicInput.eternalTransactionsHash,
287308
incomingMessagesHash:
288309
blockTrace.block.publicInput.incomingMessagesHash,
310+
blockNumber: MAX_FIELD,
289311
};
290312
const publicInput = new BlockProverPublicInput(piObject);
291313

292314
// TODO Set publicInput.stateRoot to result after block hooks!
293315
const publicOutput = new BlockProverPublicOutput({
294316
...piObject,
295-
blockNumber: Field(Field.ORDER - 1n),
296317
closed: Bool(true),
297318
});
298319

packages/sequencer/src/protocol/production/TransactionTraceService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
StateTransitionProverPublicInput,
1111
StateTransitionType,
1212
} from "@proto-kit/protocol";
13-
import { RollupMerkleTree } from "@proto-kit/common";
13+
import { MAX_FIELD, RollupMerkleTree } from "@proto-kit/common";
1414
import { Bool, Field } from "o1js";
1515
import chunk from "lodash/chunk";
1616

@@ -133,6 +133,7 @@ export class TransactionTraceService {
133133
blockHashRoot: block.block.fromBlockHashRoot,
134134
eternalTransactionsHash: block.block.fromEternalTransactionsHash,
135135
incomingMessagesHash: block.block.fromMessagesHash,
136+
blockNumber: block.block.height,
136137
});
137138

138139
return {
@@ -241,6 +242,7 @@ export class TransactionTraceService {
241242
incomingMessagesHash,
242243
networkStateHash: networkState.hash(),
243244
blockHashRoot: Field(0),
245+
blockNumber: MAX_FIELD,
244246
},
245247

246248
executionData: {

packages/sequencer/test/integration/BlockProduction.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,10 +473,17 @@ describe("block production", () => {
473473
[1, 2, 1],
474474
[1, 1, 2],
475475
[2, 2, 2],
476+
[1, 14, 0],
476477
])(
477478
"should produce multiple blocks with multiple batches with multiple transactions",
478479
async (batches, blocksPerBatch, txsPerBlock) => {
479-
expect.assertions(2 * batches + 3 * batches * blocksPerBatch);
480+
expect.assertions(
481+
2 * batches +
482+
1 * batches * blocksPerBatch +
483+
2 * batches * blocksPerBatch * txsPerBlock
484+
);
485+
486+
log.setLevel("DEBUG");
480487

481488
const sender = PrivateKey.random();
482489

@@ -511,8 +518,11 @@ describe("block production", () => {
511518
const block = await blockTrigger.produceBlock();
512519

513520
expect(block).toBeDefined();
514-
expect(block!.transactions).toHaveLength(txsPerBlock);
515-
expect(block!.transactions[0].status.toBoolean()).toBe(true);
521+
522+
for (let k = 0; k < txsPerBlock; k++) {
523+
expect(block!.transactions).toHaveLength(txsPerBlock);
524+
expect(block!.transactions[0].status.toBoolean()).toBe(true);
525+
}
516526
}
517527

518528
const batch = await blockTrigger.produceBatch();

0 commit comments

Comments
 (0)