|
1 | 1 | import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; |
2 | 2 | import { CheckpointNumber } from '@aztec/foundation/branded-types'; |
3 | 3 | import { Buffer16, Buffer32 } from '@aztec/foundation/buffer'; |
| 4 | +import { Fr } from '@aztec/foundation/curves/bn254'; |
4 | 5 | import { toArray } from '@aztec/foundation/iterable'; |
5 | 6 | import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; |
6 | 7 | import { Checkpoint, type PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; |
@@ -136,6 +137,7 @@ describe('MessageStore', () => { |
136 | 137 | const msgs2 = makeInboxMessages(3, { |
137 | 138 | initialCheckpointNumber: CheckpointNumber(20), |
138 | 139 | initialHash: msgs1.at(-1)!.rollingHash, |
| 140 | + initialInboxHash: msgs1.at(-1)!.inboxRollingHash, |
139 | 141 | }); |
140 | 142 |
|
141 | 143 | await messageStore.addL1ToL2Messages(msgs1); |
@@ -327,4 +329,164 @@ describe('MessageStore', () => { |
327 | 329 | }); |
328 | 330 | }); |
329 | 331 | }); |
| 332 | + |
| 333 | + describe('Inbox buckets', () => { |
| 334 | + // Builds `count` consecutive valid messages in a single checkpoint, then reassigns their bucket sequence and |
| 335 | + // timestamp per the given per-message spec so we can exercise multi-message and rollover buckets. |
| 336 | + const makeBucketedMessages = (spec: { seq: bigint; timestamp: bigint }[]): InboxMessage[] => { |
| 337 | + const msgs = makeInboxMessages(spec.length, { |
| 338 | + initialCheckpointNumber: CheckpointNumber(1), |
| 339 | + messagesPerCheckpoint: spec.length, |
| 340 | + }); |
| 341 | + msgs.forEach((msg, i) => { |
| 342 | + msg.bucketSeq = spec[i].seq; |
| 343 | + msg.bucketTimestamp = spec[i].timestamp; |
| 344 | + }); |
| 345 | + return msgs; |
| 346 | + }; |
| 347 | + |
| 348 | + // Three buckets over six messages: bucket 1 = [0,1,2], bucket 2 = [3,4], bucket 3 = [5]. |
| 349 | + const threeBucketSpec = [ |
| 350 | + { seq: 1n, timestamp: 100n }, |
| 351 | + { seq: 1n, timestamp: 100n }, |
| 352 | + { seq: 1n, timestamp: 100n }, |
| 353 | + { seq: 2n, timestamp: 200n }, |
| 354 | + { seq: 2n, timestamp: 200n }, |
| 355 | + { seq: 3n, timestamp: 300n }, |
| 356 | + ]; |
| 357 | + |
| 358 | + it('snapshots buckets as messages are inserted', async () => { |
| 359 | + const msgs = makeBucketedMessages(threeBucketSpec); |
| 360 | + await messageStore.addL1ToL2Messages(msgs); |
| 361 | + |
| 362 | + expect(await messageStore.getInboxBucket(1n)).toEqual({ |
| 363 | + seq: 1n, |
| 364 | + inboxRollingHash: msgs[2].inboxRollingHash, |
| 365 | + totalMsgCount: 3n, |
| 366 | + timestamp: 100n, |
| 367 | + msgCount: 3, |
| 368 | + lastMessageIndex: msgs[2].index, |
| 369 | + isOpen: false, |
| 370 | + }); |
| 371 | + expect(await messageStore.getInboxBucket(2n)).toEqual({ |
| 372 | + seq: 2n, |
| 373 | + inboxRollingHash: msgs[4].inboxRollingHash, |
| 374 | + totalMsgCount: 5n, |
| 375 | + timestamp: 200n, |
| 376 | + msgCount: 2, |
| 377 | + lastMessageIndex: msgs[4].index, |
| 378 | + isOpen: false, |
| 379 | + }); |
| 380 | + expect(await messageStore.getInboxBucket(3n)).toEqual({ |
| 381 | + seq: 3n, |
| 382 | + inboxRollingHash: msgs[5].inboxRollingHash, |
| 383 | + totalMsgCount: 6n, |
| 384 | + timestamp: 300n, |
| 385 | + msgCount: 1, |
| 386 | + lastMessageIndex: msgs[5].index, |
| 387 | + isOpen: true, |
| 388 | + }); |
| 389 | + expect(await messageStore.getInboxBucket(4n)).toBeUndefined(); |
| 390 | + }); |
| 391 | + |
| 392 | + it('continues a bucket that spans two insertion batches', async () => { |
| 393 | + const msgs = makeBucketedMessages(threeBucketSpec); |
| 394 | + await messageStore.addL1ToL2Messages(msgs.slice(0, 2)); |
| 395 | + await messageStore.addL1ToL2Messages(msgs.slice(2)); |
| 396 | + |
| 397 | + // Bucket 1 keeps accumulating across the batch boundary rather than restarting its message count. |
| 398 | + expect(await messageStore.getInboxBucket(1n)).toMatchObject({ msgCount: 3, totalMsgCount: 3n }); |
| 399 | + expect(await messageStore.getInboxBucket(3n)).toMatchObject({ msgCount: 1, totalMsgCount: 6n }); |
| 400 | + }); |
| 401 | + |
| 402 | + it('throws if the consensus rolling hash is not correct', async () => { |
| 403 | + const msgs = makeInboxMessages(5); |
| 404 | + msgs[1].inboxRollingHash = Fr.random(); |
| 405 | + await expect(messageStore.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError); |
| 406 | + }); |
| 407 | + |
| 408 | + it('resolves the latest bucket at or before a timestamp', async () => { |
| 409 | + await messageStore.addL1ToL2Messages(makeBucketedMessages(threeBucketSpec)); |
| 410 | + |
| 411 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(100n))!.seq).toEqual(1n); |
| 412 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(150n))!.seq).toEqual(1n); |
| 413 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(300n))!.seq).toEqual(3n); |
| 414 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(10_000n))!.seq).toEqual(3n); |
| 415 | + expect(await messageStore.getLatestInboxBucketAtOrBefore(99n)).toBeUndefined(); |
| 416 | + }); |
| 417 | + |
| 418 | + it('resolves rollover buckets that share a timestamp to the highest sequence', async () => { |
| 419 | + // Buckets 2 and 3 share timestamp 200 (a full bucket rolling over within the same L1 block). |
| 420 | + const msgs = makeBucketedMessages([ |
| 421 | + { seq: 1n, timestamp: 100n }, |
| 422 | + { seq: 2n, timestamp: 200n }, |
| 423 | + { seq: 3n, timestamp: 200n }, |
| 424 | + ]); |
| 425 | + await messageStore.addL1ToL2Messages(msgs); |
| 426 | + |
| 427 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(200n))!.seq).toEqual(3n); |
| 428 | + }); |
| 429 | + |
| 430 | + it('returns messages between buckets in insertion order', async () => { |
| 431 | + const msgs = makeBucketedMessages(threeBucketSpec); |
| 432 | + await messageStore.addL1ToL2Messages(msgs); |
| 433 | + const leaves = msgs.map(m => m.leaf); |
| 434 | + |
| 435 | + expect(await messageStore.getL1ToL2MessagesBetweenBuckets(0n, 3n)).toEqual(leaves); |
| 436 | + expect(await messageStore.getL1ToL2MessagesBetweenBuckets(1n, 2n)).toEqual(leaves.slice(3, 5)); |
| 437 | + expect(await messageStore.getL1ToL2MessagesBetweenBuckets(2n, 3n)).toEqual(leaves.slice(5)); |
| 438 | + // An empty (fromExclusive, toInclusive] range yields no messages. |
| 439 | + expect(await messageStore.getL1ToL2MessagesBetweenBuckets(3n, 3n)).toEqual([]); |
| 440 | + // Unknown upper bucket yields no messages. |
| 441 | + expect(await messageStore.getL1ToL2MessagesBetweenBuckets(0n, 9n)).toEqual([]); |
| 442 | + }); |
| 443 | + |
| 444 | + it('rewinds buckets when messages are removed', async () => { |
| 445 | + const msgs = makeBucketedMessages(threeBucketSpec); |
| 446 | + await messageStore.addL1ToL2Messages(msgs); |
| 447 | + |
| 448 | + // Remove the last two messages (msgs[4] in bucket 2, msgs[5] in bucket 3), splitting bucket 2. |
| 449 | + await messageStore.removeL1ToL2Messages(msgs[4].index); |
| 450 | + |
| 451 | + expect(await messageStore.getInboxBucket(3n)).toBeUndefined(); |
| 452 | + expect(await messageStore.getInboxBucket(2n)).toEqual({ |
| 453 | + seq: 2n, |
| 454 | + inboxRollingHash: msgs[3].inboxRollingHash, |
| 455 | + totalMsgCount: 4n, |
| 456 | + timestamp: 200n, |
| 457 | + msgCount: 1, |
| 458 | + lastMessageIndex: msgs[3].index, |
| 459 | + isOpen: true, |
| 460 | + }); |
| 461 | + expect(await messageStore.getInboxBucket(1n)).toMatchObject({ msgCount: 3, totalMsgCount: 3n, isOpen: false }); |
| 462 | + |
| 463 | + // Bucket 3's timestamp index entry is gone, so an at-or-before lookup falls back to bucket 2. |
| 464 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(300n))!.seq).toEqual(2n); |
| 465 | + }); |
| 466 | + |
| 467 | + it('rewinds a rollover bucket sharing a timestamp with the surviving boundary', async () => { |
| 468 | + const msgs = makeBucketedMessages([ |
| 469 | + { seq: 1n, timestamp: 100n }, |
| 470 | + { seq: 2n, timestamp: 200n }, |
| 471 | + { seq: 3n, timestamp: 200n }, |
| 472 | + ]); |
| 473 | + await messageStore.addL1ToL2Messages(msgs); |
| 474 | + |
| 475 | + // Removing the last message deletes bucket 3, whose timestamp (200) is shared with the surviving bucket 2. |
| 476 | + await messageStore.removeL1ToL2Messages(msgs[2].index); |
| 477 | + |
| 478 | + expect(await messageStore.getInboxBucket(3n)).toBeUndefined(); |
| 479 | + expect((await messageStore.getLatestInboxBucketAtOrBefore(200n))!.seq).toEqual(2n); |
| 480 | + }); |
| 481 | + |
| 482 | + it('clears all buckets when every message is removed', async () => { |
| 483 | + const msgs = makeBucketedMessages(threeBucketSpec); |
| 484 | + await messageStore.addL1ToL2Messages(msgs); |
| 485 | + |
| 486 | + await messageStore.removeL1ToL2Messages(msgs[0].index); |
| 487 | + |
| 488 | + expect(await messageStore.getInboxBucket(1n)).toBeUndefined(); |
| 489 | + expect(await messageStore.getLatestInboxBucketAtOrBefore(300n)).toBeUndefined(); |
| 490 | + }); |
| 491 | + }); |
330 | 492 | }); |
0 commit comments