@aztec/stdlib 5.0.0-rc.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/abi/buffer.d.ts +6 -2
- package/dest/abi/buffer.d.ts.map +1 -1
- package/dest/abi/buffer.js +13 -1
- package/dest/avm/avm.js +1 -1
- package/dest/aztec-address/index.d.ts +35 -5
- package/dest/aztec-address/index.d.ts.map +1 -1
- package/dest/aztec-address/index.js +32 -8
- package/dest/block/attestation_info.d.ts +1 -1
- package/dest/block/attestation_info.d.ts.map +1 -1
- package/dest/block/attestation_info.js +7 -4
- package/dest/block/l2_block_source.d.ts +44 -27
- package/dest/block/l2_block_source.d.ts.map +1 -1
- package/dest/block/l2_block_source.js +15 -1
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts +39 -0
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.d.ts.map +1 -0
- package/dest/block/l2_block_stream/event_driven_l2_block_stream.js +165 -0
- package/dest/block/l2_block_stream/index.d.ts +2 -1
- package/dest/block/l2_block_stream/index.d.ts.map +1 -1
- package/dest/block/l2_block_stream/index.js +1 -0
- package/dest/block/l2_block_stream/interfaces.d.ts +40 -6
- package/dest/block/l2_block_stream/interfaces.d.ts.map +1 -1
- package/dest/block/l2_block_stream/interfaces.js +25 -1
- package/dest/block/l2_block_stream/l2_block_stream.d.ts +23 -15
- package/dest/block/l2_block_stream/l2_block_stream.d.ts.map +1 -1
- package/dest/block/l2_block_stream/l2_block_stream.js +140 -152
- package/dest/block/l2_block_stream/l2_tips_store_base.d.ts +10 -2
- package/dest/block/l2_block_stream/l2_tips_store_base.d.ts.map +1 -1
- package/dest/block/l2_block_stream/l2_tips_store_base.js +33 -15
- package/dest/block/proposal/attestations_and_signers.d.ts +19 -1
- package/dest/block/proposal/attestations_and_signers.d.ts.map +1 -1
- package/dest/block/proposal/attestations_and_signers.js +47 -7
- package/dest/block/test/l2_tips_store_test_suite.d.ts +1 -1
- package/dest/block/test/l2_tips_store_test_suite.d.ts.map +1 -1
- package/dest/block/test/l2_tips_store_test_suite.js +94 -3
- package/dest/block/validate_block_result.d.ts +16 -1
- package/dest/block/validate_block_result.d.ts.map +1 -1
- package/dest/block/validate_block_result.js +18 -3
- package/dest/checkpoint/index.d.ts +2 -1
- package/dest/checkpoint/index.d.ts.map +1 -1
- package/dest/checkpoint/index.js +1 -0
- package/dest/checkpoint/simulation_overrides.d.ts +61 -0
- package/dest/checkpoint/simulation_overrides.d.ts.map +1 -0
- package/dest/checkpoint/simulation_overrides.js +98 -0
- package/dest/config/network-consensus-config.d.ts +1 -1
- package/dest/config/network-consensus-config.d.ts.map +1 -1
- package/dest/config/network-consensus-config.js +2 -10
- package/dest/contract/contract_address.d.ts +5 -4
- package/dest/contract/contract_address.d.ts.map +1 -1
- package/dest/contract/contract_instance.d.ts +16 -2
- package/dest/contract/contract_instance.d.ts.map +1 -1
- package/dest/contract/contract_instance.js +45 -0
- package/dest/contract/interfaces/contract_data_source.d.ts +3 -5
- package/dest/contract/interfaces/contract_data_source.d.ts.map +1 -1
- package/dest/contract/interfaces/contract_instance.d.ts +71 -10
- package/dest/contract/interfaces/contract_instance.d.ts.map +1 -1
- package/dest/contract/interfaces/contract_instance.js +7 -2
- package/dest/contract/partial_address.d.ts +9 -3
- package/dest/contract/partial_address.d.ts.map +1 -1
- package/dest/contract/partial_address.js +8 -2
- package/dest/database-version/version_manager.d.ts +18 -4
- package/dest/database-version/version_manager.d.ts.map +1 -1
- package/dest/database-version/version_manager.js +76 -6
- package/dest/file-store/factory.d.ts +1 -1
- package/dest/file-store/factory.d.ts.map +1 -1
- package/dest/file-store/factory.js +7 -0
- package/dest/file-store/index.d.ts +2 -1
- package/dest/file-store/index.d.ts.map +1 -1
- package/dest/file-store/index.js +1 -0
- package/dest/file-store/memory.d.ts +24 -0
- package/dest/file-store/memory.d.ts.map +1 -0
- package/dest/file-store/memory.js +80 -0
- package/dest/ha-signing/config.d.ts +19 -5
- package/dest/ha-signing/config.d.ts.map +1 -1
- package/dest/ha-signing/config.js +9 -3
- package/dest/ha-signing/local_config.d.ts +10 -2
- package/dest/ha-signing/local_config.d.ts.map +1 -1
- package/dest/ha-signing/local_config.js +8 -2
- package/dest/hash/hash.d.ts +1 -2
- package/dest/hash/hash.d.ts.map +1 -1
- package/dest/hash/hash.js +0 -7
- package/dest/interfaces/archiver.d.ts +8 -1
- package/dest/interfaces/archiver.d.ts.map +1 -1
- package/dest/interfaces/archiver.js +3 -2
- package/dest/interfaces/aztec-node-admin.d.ts +5 -2
- package/dest/interfaces/aztec-node-admin.d.ts.map +1 -1
- package/dest/interfaces/aztec-node-debug.d.ts +26 -1
- package/dest/interfaces/aztec-node-debug.d.ts.map +1 -1
- package/dest/interfaces/aztec-node-debug.js +20 -0
- package/dest/interfaces/aztec-node.d.ts +20 -20
- package/dest/interfaces/aztec-node.d.ts.map +1 -1
- package/dest/interfaces/aztec-node.js +8 -15
- package/dest/interfaces/chain_tips.d.ts +8 -47
- package/dest/interfaces/chain_tips.d.ts.map +1 -1
- package/dest/interfaces/chain_tips.js +1 -6
- package/dest/interfaces/checkpoint_parameter.d.ts +3 -3
- package/dest/interfaces/checkpoint_parameter.d.ts.map +1 -1
- package/dest/interfaces/checkpoint_parameter.js +3 -3
- package/dest/interfaces/configs.d.ts +4 -1
- package/dest/interfaces/configs.d.ts.map +1 -1
- package/dest/interfaces/configs.js +1 -0
- package/dest/interfaces/private_kernel_prover.d.ts +9 -11
- package/dest/interfaces/private_kernel_prover.d.ts.map +1 -1
- package/dest/interfaces/prover-node.d.ts +6 -7
- package/dest/interfaces/prover-node.d.ts.map +1 -1
- package/dest/interfaces/prover-node.js +15 -10
- package/dest/interfaces/proving-job.d.ts +87 -74
- package/dest/interfaces/proving-job.d.ts.map +1 -1
- package/dest/interfaces/proving-job.js +13 -4
- package/dest/interfaces/server_circuit_prover.d.ts +3 -3
- package/dest/interfaces/server_circuit_prover.d.ts.map +1 -1
- package/dest/interfaces/validator.d.ts +7 -3
- package/dest/interfaces/validator.d.ts.map +1 -1
- package/dest/interfaces/world_state.d.ts +14 -1
- package/dest/interfaces/world_state.d.ts.map +1 -1
- package/dest/kernel/hints/find_private_kernel_reset_dimensions.d.ts +8 -2
- package/dest/kernel/hints/find_private_kernel_reset_dimensions.d.ts.map +1 -1
- package/dest/kernel/hints/find_private_kernel_reset_dimensions.js +12 -74
- package/dest/kernel/hints/read_request.js +1 -1
- package/dest/kernel/index.d.ts +2 -2
- package/dest/kernel/index.d.ts.map +1 -1
- package/dest/kernel/index.js +1 -1
- package/dest/kernel/log_hash.d.ts +1 -1
- package/dest/kernel/log_hash.d.ts.map +1 -1
- package/dest/kernel/log_hash.js +2 -2
- package/dest/kernel/note_hash.js +1 -1
- package/dest/kernel/nullifier.js +1 -1
- package/dest/kernel/private_circuit_public_inputs.d.ts +13 -3
- package/dest/kernel/private_circuit_public_inputs.d.ts.map +1 -1
- package/dest/kernel/private_circuit_public_inputs.js +14 -7
- package/dest/kernel/private_context_inputs.d.ts +4 -3
- package/dest/kernel/private_context_inputs.d.ts.map +1 -1
- package/dest/kernel/private_context_inputs.js +7 -3
- package/dest/kernel/private_kernel_circuit_public_inputs.d.ts +7 -1
- package/dest/kernel/private_kernel_circuit_public_inputs.d.ts.map +1 -1
- package/dest/kernel/private_kernel_circuit_public_inputs.js +7 -0
- package/dest/kernel/private_kernel_init_2_circuit_private_inputs.d.ts +2 -3
- package/dest/kernel/private_kernel_init_2_circuit_private_inputs.d.ts.map +1 -1
- package/dest/kernel/private_kernel_init_2_circuit_private_inputs.js +3 -5
- package/dest/kernel/private_kernel_init_3_circuit_private_inputs.d.ts +2 -3
- package/dest/kernel/private_kernel_init_3_circuit_private_inputs.d.ts.map +1 -1
- package/dest/kernel/private_kernel_init_3_circuit_private_inputs.js +3 -5
- package/dest/kernel/private_kernel_init_circuit_private_inputs.d.ts +1 -9
- package/dest/kernel/private_kernel_init_circuit_private_inputs.d.ts.map +1 -1
- package/dest/kernel/private_kernel_init_circuit_private_inputs.js +2 -6
- package/dest/kernel/private_kernel_reset_dimensions.d.ts +7 -8
- package/dest/kernel/private_kernel_reset_dimensions.d.ts.map +1 -1
- package/dest/kernel/private_kernel_reset_tail_circuit_private_inputs.d.ts +29 -0
- package/dest/kernel/private_kernel_reset_tail_circuit_private_inputs.d.ts.map +1 -0
- package/dest/kernel/private_kernel_reset_tail_circuit_private_inputs.js +36 -0
- package/dest/kernel/private_log_data.d.ts +1 -1
- package/dest/kernel/private_log_data.d.ts.map +1 -1
- package/dest/kernel/private_log_data.js +1 -1
- package/dest/keys/derivation.d.ts +41 -6
- package/dest/keys/derivation.d.ts.map +1 -1
- package/dest/keys/derivation.js +49 -26
- package/dest/logs/app_tagging_secret.d.ts +30 -7
- package/dest/logs/app_tagging_secret.d.ts.map +1 -1
- package/dest/logs/app_tagging_secret.js +46 -39
- package/dest/logs/contract_class_log.d.ts +1 -2
- package/dest/logs/contract_class_log.d.ts.map +1 -1
- package/dest/logs/contract_class_log.js +0 -4
- package/dest/logs/index.d.ts +1 -3
- package/dest/logs/index.d.ts.map +1 -1
- package/dest/logs/index.js +0 -2
- package/dest/logs/shared_secret_derivation.d.ts +19 -6
- package/dest/logs/shared_secret_derivation.d.ts.map +1 -1
- package/dest/logs/shared_secret_derivation.js +26 -11
- package/dest/messaging/l1_to_l2_message.d.ts +23 -2
- package/dest/messaging/l1_to_l2_message.d.ts.map +1 -1
- package/dest/messaging/l1_to_l2_message.js +36 -11
- package/dest/p2p/attestation_utils.d.ts +1 -1
- package/dest/p2p/attestation_utils.d.ts.map +1 -1
- package/dest/p2p/attestation_utils.js +8 -1
- package/dest/p2p/checkpoint_attestation.d.ts +10 -1
- package/dest/p2p/checkpoint_attestation.d.ts.map +1 -1
- package/dest/p2p/checkpoint_attestation.js +10 -0
- package/dest/p2p/checkpoint_proposal.d.ts +2 -1
- package/dest/p2p/checkpoint_proposal.d.ts.map +1 -1
- package/dest/p2p/consensus_payload.d.ts +2 -1
- package/dest/p2p/consensus_payload.d.ts.map +1 -1
- package/dest/p2p/signature_utils.d.ts +10 -1
- package/dest/p2p/signature_utils.d.ts.map +1 -1
- package/dest/p2p/signature_utils.js +9 -1
- package/dest/rollup/avm_proof_data.d.ts +3 -3
- package/dest/rollup/avm_proof_data.d.ts.map +1 -1
- package/dest/rollup/checkpoint_header.d.ts +8 -3
- package/dest/rollup/checkpoint_header.d.ts.map +1 -1
- package/dest/rollup/checkpoint_header.js +20 -10
- package/dest/stats/stats.d.ts +2 -2
- package/dest/stats/stats.d.ts.map +1 -1
- package/dest/tests/factories.d.ts +1 -1
- package/dest/tests/factories.d.ts.map +1 -1
- package/dest/tests/factories.js +7 -15
- package/dest/trees/nullifier_membership_witness.d.ts +5 -8
- package/dest/trees/nullifier_membership_witness.d.ts.map +1 -1
- package/dest/trees/nullifier_membership_witness.js +0 -9
- package/dest/trees/public_data_witness.d.ts +7 -17
- package/dest/trees/public_data_witness.d.ts.map +1 -1
- package/dest/trees/public_data_witness.js +0 -27
- package/dest/tx/execution_payload.d.ts +1 -1
- package/dest/tx/execution_payload.d.ts.map +1 -1
- package/dest/tx/execution_payload.js +1 -1
- package/dest/tx/global_variable_builder.d.ts +2 -12
- package/dest/tx/global_variable_builder.d.ts.map +1 -1
- package/dest/tx/global_variables.js +2 -2
- package/dest/tx/private_tx_constant_data.d.ts +18 -2
- package/dest/tx/private_tx_constant_data.d.ts.map +1 -1
- package/dest/tx/private_tx_constant_data.js +13 -4
- package/dest/tx/simulated_tx.d.ts +10 -6
- package/dest/tx/simulated_tx.d.ts.map +1 -1
- package/dest/world-state/genesis_data.d.ts +9 -1
- package/dest/world-state/genesis_data.d.ts.map +1 -1
- package/dest/world-state/genesis_data.js +1 -0
- package/package.json +8 -8
- package/src/abi/buffer.ts +13 -1
- package/src/avm/avm.ts +1 -1
- package/src/aztec-address/index.ts +36 -6
- package/src/block/attestation_info.ts +7 -2
- package/src/block/l2_block_source.ts +64 -17
- package/src/block/l2_block_stream/event_driven_l2_block_stream.ts +207 -0
- package/src/block/l2_block_stream/index.ts +1 -0
- package/src/block/l2_block_stream/interfaces.ts +60 -6
- package/src/block/l2_block_stream/l2_block_stream.ts +201 -178
- package/src/block/l2_block_stream/l2_tips_store_base.ts +36 -15
- package/src/block/proposal/attestations_and_signers.ts +53 -7
- package/src/block/test/l2_tips_store_test_suite.ts +73 -4
- package/src/block/validate_block_result.ts +48 -2
- package/src/checkpoint/index.ts +1 -0
- package/src/checkpoint/simulation_overrides.ts +171 -0
- package/src/config/network-consensus-config.ts +2 -13
- package/src/contract/contract_address.ts +4 -3
- package/src/contract/contract_instance.ts +62 -1
- package/src/contract/interfaces/contract_data_source.ts +2 -4
- package/src/contract/interfaces/contract_instance.ts +26 -8
- package/src/contract/partial_address.ts +8 -2
- package/src/database-version/version_manager.ts +96 -6
- package/src/file-store/factory.ts +7 -0
- package/src/file-store/index.ts +1 -0
- package/src/file-store/memory.ts +94 -0
- package/src/ha-signing/config.ts +23 -5
- package/src/ha-signing/local_config.ts +20 -1
- package/src/hash/hash.ts +0 -8
- package/src/interfaces/archiver.ts +9 -1
- package/src/interfaces/aztec-node-debug.ts +37 -0
- package/src/interfaces/aztec-node.ts +30 -33
- package/src/interfaces/chain_tips.ts +8 -16
- package/src/interfaces/checkpoint_parameter.ts +3 -3
- package/src/interfaces/configs.ts +3 -0
- package/src/interfaces/private_kernel_prover.ts +10 -12
- package/src/interfaces/prover-node.ts +19 -11
- package/src/interfaces/proving-job.ts +15 -3
- package/src/interfaces/server_circuit_prover.ts +2 -2
- package/src/interfaces/world_state.ts +14 -0
- package/src/kernel/hints/find_private_kernel_reset_dimensions.ts +22 -112
- package/src/kernel/hints/read_request.ts +1 -1
- package/src/kernel/index.ts +1 -1
- package/src/kernel/log_hash.ts +5 -2
- package/src/kernel/note_hash.ts +1 -1
- package/src/kernel/nullifier.ts +1 -1
- package/src/kernel/private_circuit_public_inputs.ts +12 -1
- package/src/kernel/private_context_inputs.ts +10 -3
- package/src/kernel/private_kernel_circuit_public_inputs.ts +9 -0
- package/src/kernel/private_kernel_init_2_circuit_private_inputs.ts +0 -3
- package/src/kernel/private_kernel_init_3_circuit_private_inputs.ts +0 -3
- package/src/kernel/private_kernel_init_circuit_private_inputs.ts +0 -6
- package/src/kernel/private_kernel_reset_dimensions.ts +15 -5
- package/src/kernel/private_kernel_reset_tail_circuit_private_inputs.ts +69 -0
- package/src/kernel/private_log_data.ts +4 -1
- package/src/keys/derivation.ts +69 -24
- package/src/logs/app_tagging_secret.ts +62 -39
- package/src/logs/contract_class_log.ts +0 -9
- package/src/logs/index.ts +0 -2
- package/src/logs/shared_secret_derivation.ts +31 -15
- package/src/messaging/l1_to_l2_message.ts +51 -13
- package/src/p2p/attestation_utils.ts +8 -1
- package/src/p2p/checkpoint_attestation.ts +12 -0
- package/src/p2p/signature_utils.ts +9 -0
- package/src/rollup/avm_proof_data.ts +2 -2
- package/src/rollup/checkpoint_header.ts +16 -2
- package/src/stats/stats.ts +2 -2
- package/src/tests/factories.ts +6 -12
- package/src/trees/nullifier_membership_witness.ts +0 -11
- package/src/trees/public_data_witness.ts +0 -31
- package/src/tx/execution_payload.ts +2 -1
- package/src/tx/global_variable_builder.ts +1 -17
- package/src/tx/global_variables.ts +2 -2
- package/src/tx/private_tx_constant_data.ts +25 -2
- package/src/world-state/genesis_data.ts +10 -0
- package/dest/kernel/private_kernel_tail_circuit_private_inputs.d.ts +0 -47
- package/dest/kernel/private_kernel_tail_circuit_private_inputs.d.ts.map +0 -1
- package/dest/kernel/private_kernel_tail_circuit_private_inputs.js +0 -38
- package/dest/logs/message_context.d.ts +0 -27
- package/dest/logs/message_context.d.ts.map +0 -1
- package/dest/logs/message_context.js +0 -74
- package/dest/logs/pending_tagged_log.d.ts +0 -13
- package/dest/logs/pending_tagged_log.d.ts.map +0 -1
- package/dest/logs/pending_tagged_log.js +0 -34
- package/src/kernel/private_kernel_tail_circuit_private_inputs.ts +0 -58
- package/src/logs/message_context.ts +0 -76
- package/src/logs/pending_tagged_log.ts +0 -39
|
@@ -1,14 +1,37 @@
|
|
|
1
|
-
import { BlockNumber
|
|
1
|
+
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
2
2
|
import { AbortError } from '@aztec/foundation/error';
|
|
3
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
5
5
|
|
|
6
|
-
import type {
|
|
7
|
-
import { type L2BlockId, type L2BlockSource, makeL2BlockId } from '../l2_block_source.js';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
import type { L2Block } from '../l2_block.js';
|
|
7
|
+
import { type L2BlockId, type L2BlockSource, type LocalL2Tips, makeL2BlockId } from '../l2_block_source.js';
|
|
8
|
+
import {
|
|
9
|
+
type L2BlockStreamEvent,
|
|
10
|
+
type L2BlockStreamEventHandler,
|
|
11
|
+
type L2BlockStreamLocalDataProvider,
|
|
12
|
+
localBlockIdDiffers,
|
|
13
|
+
} from './interfaces.js';
|
|
14
|
+
|
|
15
|
+
/** Subset of the block source the stream depends on. Checkpoint payloads are no longer fetched here. */
|
|
16
|
+
export type L2BlockStreamSource = Pick<L2BlockSource, 'getBlocks' | 'getBlockData' | 'getL2Tips'>;
|
|
17
|
+
|
|
18
|
+
/** Options accepted by {@link L2BlockStream} and {@link EventDrivenL2BlockStream}. */
|
|
19
|
+
export type L2BlockStreamOptions = {
|
|
20
|
+
pollIntervalMS?: number;
|
|
21
|
+
batchSize?: number;
|
|
22
|
+
startingBlock?: number;
|
|
23
|
+
/** Instead of downloading all blocks, only fetch the smallest subset that results in reliable reorg detection. */
|
|
24
|
+
skipFinalized?: boolean;
|
|
25
|
+
/** When true, checkpoint events will not be emitted. Blocks are still fetched but only blocks-added events are emitted. */
|
|
26
|
+
ignoreCheckpoints?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* When true, the block download loop is skipped entirely: `getBlocks` is never called and `blocks-added` is
|
|
29
|
+
* never emitted. Only the tip events (`chain-proposed`/`chain-checkpointed`/`chain-proven`/`chain-finalized`)
|
|
30
|
+
* and `chain-pruned` are emitted, driven by the `getL2Tips` snapshot. For consumers that track tips but never
|
|
31
|
+
* consume block payloads.
|
|
32
|
+
*/
|
|
33
|
+
tipsOnly?: boolean;
|
|
34
|
+
};
|
|
12
35
|
|
|
13
36
|
/** Creates a stream of events for new blocks, chain tips updates, and reorgs, out of polling an archiver or a node. */
|
|
14
37
|
export class L2BlockStream {
|
|
@@ -17,22 +40,18 @@ export class L2BlockStream {
|
|
|
17
40
|
private hasStarted = false;
|
|
18
41
|
|
|
19
42
|
constructor(
|
|
20
|
-
private l2BlockSource:
|
|
43
|
+
private l2BlockSource: L2BlockStreamSource,
|
|
21
44
|
private localData: L2BlockStreamLocalDataProvider,
|
|
22
45
|
private handler: L2BlockStreamEventHandler,
|
|
23
46
|
private readonly log = createLogger('types:block_stream'),
|
|
24
|
-
private opts: {
|
|
25
|
-
pollIntervalMS?: number;
|
|
26
|
-
batchSize?: number;
|
|
27
|
-
startingBlock?: number;
|
|
28
|
-
/** Instead of downloading all blocks, only fetch the smallest subset that results in reliable reorg detection. */
|
|
29
|
-
skipFinalized?: boolean;
|
|
30
|
-
/** When true, checkpoint events will not be emitted. Blocks are still fetched via checkpoints but only blocks-added events are emitted. */
|
|
31
|
-
ignoreCheckpoints?: boolean;
|
|
32
|
-
/** Maximum number of checkpoints to prefetch at once during sync. Defaults to CHECKPOINT_PREFETCH_LIMIT (50). */
|
|
33
|
-
checkpointPrefetchLimit?: number;
|
|
34
|
-
} = {},
|
|
47
|
+
private opts: L2BlockStreamOptions = {},
|
|
35
48
|
) {
|
|
49
|
+
if (opts.tipsOnly && (opts.startingBlock !== undefined || opts.batchSize !== undefined || opts.skipFinalized)) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'tipsOnly is incompatible with startingBlock, batchSize, and skipFinalized: all three are ' +
|
|
52
|
+
'block-download options and there is no download loop in tips-only mode.',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
36
55
|
// Note that RunningPromise is in stopped state by default. This promise won't run until someone invokes `start`,
|
|
37
56
|
// which makes it run periodically, or `sync`, which triggers it once.
|
|
38
57
|
// Users of L2BlockStream decide what mode to run it in (_periodically_ vs _manually triggered_).
|
|
@@ -66,7 +85,8 @@ export class L2BlockStream {
|
|
|
66
85
|
|
|
67
86
|
protected async work() {
|
|
68
87
|
try {
|
|
69
|
-
|
|
88
|
+
// The source tips snapshot is the plan for this pass; it is re-read after the walk-back if a divergence is found.
|
|
89
|
+
let sourceTips = await this.l2BlockSource.getL2Tips();
|
|
70
90
|
const localTips = await this.localData.getL2Tips();
|
|
71
91
|
this.log.trace(`Running L2 block stream`, { sourceTips, localTips });
|
|
72
92
|
|
|
@@ -77,16 +97,26 @@ export class L2BlockStream {
|
|
|
77
97
|
);
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
//
|
|
100
|
+
// Baseline for the chain-proposed event; captured before the local store mutates during the pass.
|
|
101
|
+
const prePassProposed = localTips.proposed;
|
|
102
|
+
|
|
103
|
+
// Walk back to find a reorg, floored at the local finalized tip (a legitimate reorg can never reach it, since
|
|
104
|
+
// finalized means the proving tx is itself L1-finalized). Seed the cache with ONLY the proposed tip: a stale
|
|
105
|
+
// tier seed at a reorged height equals the local old-fork hash, faking agreement and stopping the walk above the
|
|
106
|
+
// true divergence (an under-deep prune no later pass re-detects), whereas a stale proposed seed only masks the
|
|
107
|
+
// tip for one pass.
|
|
81
108
|
let latestBlockNumber = localTips.proposed.number;
|
|
82
109
|
const sourceCache = new BlockHashCache([sourceTips.proposed]);
|
|
83
|
-
|
|
110
|
+
const walkFloor = localTips.finalized.block.number;
|
|
111
|
+
while (
|
|
112
|
+
!(await this.areBlockHashesEqualAt(latestBlockNumber, {
|
|
113
|
+
sourceCache,
|
|
114
|
+
sourceProposed: sourceTips.proposed.number,
|
|
115
|
+
}))
|
|
116
|
+
) {
|
|
84
117
|
if (latestBlockNumber === 0) {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// they were configured with different `genesisTimestamp`/prefilled state. Continuing
|
|
88
|
-
// would underflow into negative block numbers and surface as "block hash not found
|
|
89
|
-
// for -1" further down. Fail loudly with a meaningful error instead.
|
|
118
|
+
// Walked back to genesis and the hashes still differ: the two sides disagree on block 0 itself (usually
|
|
119
|
+
// different genesisTimestamp/prefilled state). Fail loudly rather than underflow into negative heights.
|
|
90
120
|
this.log.error(`Genesis block hash mismatch between local store and source`, {
|
|
91
121
|
localBlockHash: await this.localData.getL2BlockHash(BlockNumber.ZERO),
|
|
92
122
|
sourceBlockHash: sourceCache.get(0) ?? (await this.getBlockHashFromSource(BlockNumber.ZERO)),
|
|
@@ -97,10 +127,30 @@ export class L2BlockStream {
|
|
|
97
127
|
'(e.g. genesisTimestamp or prefilled public data).',
|
|
98
128
|
);
|
|
99
129
|
}
|
|
130
|
+
if (latestBlockNumber <= walkFloor) {
|
|
131
|
+
// A mismatch at or below the finalized tip cannot be a reorg (it contradicts L1-finalized state), so stop
|
|
132
|
+
// here and prune at most to the finalized tip rather than pruning finalized state on non-reorg evidence.
|
|
133
|
+
this.log.warn(`Block hash mismatch at or below the local finalized tip; stopping the walk-back here`, {
|
|
134
|
+
blockNumber: latestBlockNumber,
|
|
135
|
+
finalizedBlockNumber: walkFloor,
|
|
136
|
+
localBlockHash: await this.localData.getL2BlockHash(latestBlockNumber),
|
|
137
|
+
sourceBlockHash:
|
|
138
|
+
sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber)),
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
100
142
|
latestBlockNumber--;
|
|
101
143
|
}
|
|
102
144
|
|
|
145
|
+
let pruned = false;
|
|
103
146
|
if (latestBlockNumber < localTips.proposed.number) {
|
|
147
|
+
// Re-read the source tips after the (possibly slow) walk-back so the prune event carries fresh checkpointed
|
|
148
|
+
// and proven tips, and the prune-target clamp, download plan, and tier reconciliation track the post-prune
|
|
149
|
+
// source chain. Append only the re-read proposed tip: it is a fresh entry that cannot poison the (already
|
|
150
|
+
// finished) walk and serves the prune-event hash lookup below.
|
|
151
|
+
sourceTips = await this.l2BlockSource.getL2Tips();
|
|
152
|
+
sourceCache.add(sourceTips.proposed);
|
|
153
|
+
|
|
104
154
|
latestBlockNumber = BlockNumber(Math.min(latestBlockNumber, sourceTips.proposed.number)); // see #13471
|
|
105
155
|
const hash = sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber));
|
|
106
156
|
if (latestBlockNumber !== 0 && !hash) {
|
|
@@ -115,169 +165,48 @@ export class L2BlockStream {
|
|
|
115
165
|
checkpointed: sourceTips.checkpointed,
|
|
116
166
|
proven: sourceTips.proven,
|
|
117
167
|
});
|
|
168
|
+
pruned = true;
|
|
118
169
|
}
|
|
119
170
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
171
|
+
// Pass atomicity: a prune mid-download leaves the source unable to serve the planned blocks, so the snapshot's
|
|
172
|
+
// tier tips may reference blocks the consumer never saw — skip tier reconciliation in that case. Tips-only mode
|
|
173
|
+
// has no download plan (the snapshot is one atomic getL2Tips read), so it always reconciles.
|
|
174
|
+
if (!this.opts.tipsOnly && !(await this.downloadBlocks(latestBlockNumber, sourceTips))) {
|
|
175
|
+
return;
|
|
124
176
|
}
|
|
125
177
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
178
|
+
// End-of-pass reconciliation: chain-proposed fires against the pre-pass baseline (a post-prune re-read would
|
|
179
|
+
// equal the source tip and suppress it), then the tiers highest-to-lowest so the finalized <= proven <=
|
|
180
|
+
// checkpointed <= proposed invariant holds mid-pass.
|
|
181
|
+
if (localBlockIdDiffers(prePassProposed, sourceTips.proposed)) {
|
|
182
|
+
await this.emitEvent({ type: 'chain-proposed', block: sourceTips.proposed });
|
|
130
183
|
}
|
|
131
184
|
|
|
132
|
-
|
|
133
|
-
// When checkpoints are ignored the local provider may omit `checkpointed`; in that case the fallback to
|
|
134
|
-
// CheckpointNumber.ZERO is harmless because `nextCheckpointToEmit` is never consumed for emission (Loop 1 and
|
|
135
|
-
// the startingBlock/skipFinalized adjustments below only feed checkpoint emission, which is gated off).
|
|
136
|
-
let nextCheckpointToEmit = CheckpointNumber(
|
|
137
|
-
(localTips.checkpointed?.checkpoint.number ?? CheckpointNumber.ZERO) + 1,
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
// When startingBlock is set, also skip ahead for checkpoints.
|
|
185
|
+
const reconcileTips = pruned ? await this.localData.getL2Tips() : localTips;
|
|
141
186
|
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
nextCheckpointToEmit <= sourceTips.checkpointed.checkpoint.number
|
|
187
|
+
!this.opts.ignoreCheckpoints &&
|
|
188
|
+
localBlockIdDiffers(reconcileTips.checkpointed?.block, sourceTips.checkpointed.block)
|
|
145
189
|
) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (startingBlockData) {
|
|
152
|
-
nextCheckpointToEmit = CheckpointNumber(Math.max(nextCheckpointToEmit, startingBlockData.checkpointNumber));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (this.opts.skipFinalized) {
|
|
158
|
-
// When skipping finalized blocks we need to provide reliable reorg detection while fetching as few blocks as
|
|
159
|
-
// possible. Finalized blocks cannot be reorged by definition, so we can skip most of them. We do need the very
|
|
160
|
-
// last finalized block however in order to guarantee that we will eventually find a block in which our local
|
|
161
|
-
// store matches the source.
|
|
162
|
-
// If the last finalized block is behind our local tip, there is nothing to skip.
|
|
163
|
-
nextBlockNumber = Math.max(sourceTips.finalized.block.number, nextBlockNumber);
|
|
164
|
-
// If the next checkpoint to emit is behind the finalized tip then skip forward
|
|
165
|
-
nextCheckpointToEmit = CheckpointNumber(Math.max(nextCheckpointToEmit, sourceTips.finalized.checkpoint.number));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Loop 1: Emit checkpoint events for checkpoints whose blocks are already in local storage.
|
|
169
|
-
// This handles the case where blocks were synced as uncheckpointed and later became checkpointed.
|
|
170
|
-
// The guard `lastBlockInCheckpoint.number > localTips.proposed.number` ensures we don't emit
|
|
171
|
-
// checkpoints for blocks we don't have (e.g., when startingBlock skips earlier blocks).
|
|
172
|
-
// Since only one checkpoint can ever be uncheckpointed, this loop should iterate at most once.
|
|
173
|
-
if (!this.opts.ignoreCheckpoints) {
|
|
174
|
-
let loop1Iterations = 0;
|
|
175
|
-
while (nextCheckpointToEmit <= sourceTips.checkpointed.checkpoint.number) {
|
|
176
|
-
const checkpoints = await this.l2BlockSource.getCheckpoints({ from: nextCheckpointToEmit, limit: 1 });
|
|
177
|
-
if (checkpoints.length === 0) {
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
const lastBlockInCheckpoint = checkpoints[0].checkpoint.blocks.at(-1)!;
|
|
181
|
-
// If this checkpoint has blocks we haven't seen yet, stop - they need to be fetched first
|
|
182
|
-
if (lastBlockInCheckpoint.number > localTips.proposed.number) {
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
loop1Iterations++;
|
|
186
|
-
if (loop1Iterations > 1) {
|
|
187
|
-
this.log.warn(
|
|
188
|
-
`Emitting multiple checkpoints (${loop1Iterations}) for already-local blocks. ` +
|
|
189
|
-
`Next checkpoint: ${nextCheckpointToEmit}, source checkpointed: ${sourceTips.checkpointed.checkpoint.number}`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
const lastBlockHash = await lastBlockInCheckpoint.hash();
|
|
193
|
-
await this.emitEvent({
|
|
194
|
-
type: 'chain-checkpointed',
|
|
195
|
-
checkpoint: checkpoints[0],
|
|
196
|
-
block: makeL2BlockId(lastBlockInCheckpoint.number, lastBlockHash.toString()),
|
|
197
|
-
});
|
|
198
|
-
nextCheckpointToEmit = CheckpointNumber(nextCheckpointToEmit + 1);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Loop 2: Fetch new checkpointed blocks. For each checkpoint, emit all blocks
|
|
203
|
-
// from that checkpoint that we need, then emit the checkpoint event.
|
|
204
|
-
// We prefetch multiple checkpoints, then process them one by one.
|
|
205
|
-
let prefetchedCheckpoints: PublishedCheckpoint[] = [];
|
|
206
|
-
let prefetchIdx = 0;
|
|
207
|
-
let nextCheckpointNumber: CheckpointNumber | undefined;
|
|
208
|
-
|
|
209
|
-
// Find the starting checkpoint number
|
|
210
|
-
if (nextBlockNumber <= sourceTips.checkpointed.block.number) {
|
|
211
|
-
const blockData = await this.l2BlockSource.getBlockData({ number: BlockNumber(nextBlockNumber) });
|
|
212
|
-
if (blockData) {
|
|
213
|
-
nextCheckpointNumber = blockData.checkpointNumber;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
while (nextBlockNumber <= sourceTips.checkpointed.block.number && nextCheckpointNumber !== undefined) {
|
|
218
|
-
// Refill the prefetch buffer when exhausted
|
|
219
|
-
if (prefetchIdx >= prefetchedCheckpoints.length) {
|
|
220
|
-
const prefetchLimit = this.opts.checkpointPrefetchLimit ?? CHECKPOINT_PREFETCH_LIMIT;
|
|
221
|
-
prefetchedCheckpoints = await this.l2BlockSource.getCheckpoints({
|
|
222
|
-
from: nextCheckpointNumber,
|
|
223
|
-
limit: prefetchLimit,
|
|
224
|
-
});
|
|
225
|
-
prefetchIdx = 0;
|
|
226
|
-
if (prefetchedCheckpoints.length === 0) {
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const checkpoint = prefetchedCheckpoints[prefetchIdx]!;
|
|
232
|
-
|
|
233
|
-
// Get all blocks from this checkpoint that we need, respecting batchSize
|
|
234
|
-
const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.checkpointed.block.number - nextBlockNumber + 1);
|
|
235
|
-
const blocksForCheckpoint = checkpoint.checkpoint.blocks
|
|
236
|
-
.filter(b => b.number >= nextBlockNumber)
|
|
237
|
-
.slice(0, limit);
|
|
238
|
-
if (blocksForCheckpoint.length === 0) {
|
|
239
|
-
break;
|
|
240
|
-
}
|
|
241
|
-
await this.emitEvent({ type: 'blocks-added', blocks: blocksForCheckpoint });
|
|
242
|
-
nextBlockNumber = blocksForCheckpoint.at(-1)!.number + 1;
|
|
243
|
-
|
|
244
|
-
// If we've reached the end of this checkpoint, emit the checkpoint event and move to next
|
|
245
|
-
const lastBlockInCheckpoint = checkpoint.checkpoint.blocks.at(-1)!;
|
|
246
|
-
if (nextBlockNumber > lastBlockInCheckpoint.number) {
|
|
247
|
-
if (!this.opts.ignoreCheckpoints) {
|
|
248
|
-
const lastBlockHash = await lastBlockInCheckpoint.hash();
|
|
249
|
-
await this.emitEvent({
|
|
250
|
-
type: 'chain-checkpointed',
|
|
251
|
-
checkpoint,
|
|
252
|
-
block: makeL2BlockId(lastBlockInCheckpoint.number, lastBlockHash.toString()),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
prefetchIdx++;
|
|
256
|
-
nextCheckpointNumber = CheckpointNumber(nextCheckpointNumber + 1);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Loop 3: Fetch any remaining uncheckpointed (proposed) blocks.
|
|
261
|
-
while (nextBlockNumber <= sourceTips.proposed.number) {
|
|
262
|
-
const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.proposed.number - nextBlockNumber + 1);
|
|
263
|
-
this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit}`);
|
|
264
|
-
const blocks = await this.l2BlockSource.getBlocks({ from: BlockNumber(nextBlockNumber), limit });
|
|
265
|
-
if (blocks.length === 0) {
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
await this.emitEvent({ type: 'blocks-added', blocks });
|
|
269
|
-
nextBlockNumber = blocks.at(-1)!.number + 1;
|
|
190
|
+
await this.emitEvent({
|
|
191
|
+
type: 'chain-checkpointed',
|
|
192
|
+
block: sourceTips.checkpointed.block,
|
|
193
|
+
checkpoint: sourceTips.checkpointed.checkpoint,
|
|
194
|
+
});
|
|
270
195
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
196
|
+
if (
|
|
197
|
+
reconcileTips.proven !== undefined &&
|
|
198
|
+
localBlockIdDiffers(reconcileTips.proven.block, sourceTips.proven.block)
|
|
199
|
+
) {
|
|
274
200
|
await this.emitEvent({
|
|
275
201
|
type: 'chain-proven',
|
|
276
202
|
block: sourceTips.proven.block,
|
|
277
203
|
checkpoint: sourceTips.proven.checkpoint,
|
|
278
204
|
});
|
|
279
205
|
}
|
|
280
|
-
if (
|
|
206
|
+
if (
|
|
207
|
+
reconcileTips.finalized !== undefined &&
|
|
208
|
+
localBlockIdDiffers(reconcileTips.finalized.block, sourceTips.finalized.block)
|
|
209
|
+
) {
|
|
281
210
|
await this.emitEvent({
|
|
282
211
|
type: 'chain-finalized',
|
|
283
212
|
block: sourceTips.finalized.block,
|
|
@@ -292,12 +221,84 @@ export class L2BlockStream {
|
|
|
292
221
|
}
|
|
293
222
|
}
|
|
294
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Downloads every block from the post-prune cursor through the source's proposed tip, emitting `blocks-added`
|
|
226
|
+
* events. The return value gates tier-cursor advancement (pass atomicity): tier tips may only be reconciled when
|
|
227
|
+
* the plan that backs them ran to the proposed tip.
|
|
228
|
+
* @returns `true` if the plan completed (caught up, or delivered the proposed tip with a matching hash); `false` if
|
|
229
|
+
* the source no longer has a promised block, or served a fork at the proposed height mid-pass.
|
|
230
|
+
*/
|
|
231
|
+
private async downloadBlocks(latestBlockNumber: BlockNumber, sourceTips: LocalL2Tips): Promise<boolean> {
|
|
232
|
+
// The post-prune cursor: the highest block number both sides agree on. Block downloads resume from here.
|
|
233
|
+
let nextBlockNumber = latestBlockNumber + 1;
|
|
234
|
+
|
|
235
|
+
// From a fresh local store, fast-forward past history the consumer doesn't care about.
|
|
236
|
+
const startingBlock = this.opts.startingBlock !== undefined ? BlockNumber(this.opts.startingBlock) : undefined;
|
|
237
|
+
if (latestBlockNumber === 0 && startingBlock !== undefined) {
|
|
238
|
+
nextBlockNumber = Math.max(startingBlock, 1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (this.opts.skipFinalized) {
|
|
242
|
+
// Finalized blocks cannot be reorged, so skip them — but keep the last finalized block as the guaranteed point
|
|
243
|
+
// where local and source agree, the floor the walk-back terminates against.
|
|
244
|
+
nextBlockNumber = Math.max(sourceTips.finalized.block.number, nextBlockNumber);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!this.hasStarted) {
|
|
248
|
+
this.log.verbose(`Starting sync from block number ${nextBlockNumber - 1}`);
|
|
249
|
+
this.hasStarted = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let lastDeliveredBlock: L2Block | undefined;
|
|
253
|
+
|
|
254
|
+
while (nextBlockNumber <= sourceTips.proposed.number) {
|
|
255
|
+
const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.proposed.number - nextBlockNumber + 1);
|
|
256
|
+
this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit}`);
|
|
257
|
+
const blocks = await this.l2BlockSource.getBlocks({ from: BlockNumber(nextBlockNumber), limit });
|
|
258
|
+
if (blocks.length === 0) {
|
|
259
|
+
// The source no longer has a block the snapshot promised: the snapshot is provably stale, so report the plan
|
|
260
|
+
// incomplete and skip reconciliation this pass.
|
|
261
|
+
this.log.warn(`Block source returned no blocks for a promised range; skipping reconciliation this pass`, {
|
|
262
|
+
from: nextBlockNumber,
|
|
263
|
+
limit,
|
|
264
|
+
sourceProposed: sourceTips.proposed.number,
|
|
265
|
+
});
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
await this.emitEvent({ type: 'blocks-added', blocks });
|
|
269
|
+
lastDeliveredBlock = blocks.at(-1)!;
|
|
270
|
+
nextBlockNumber = lastDeliveredBlock.number + 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (lastDeliveredBlock === undefined) {
|
|
274
|
+
// Loop never ran: caught up before the plan started, or startingBlock past the tip (A-1061). Trivially complete.
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Complete iff the block delivered at the proposed height carries the snapshot's proposed hash; a different hash
|
|
279
|
+
// means a same-height fork swap happened mid-pass, so the snapshot is stale.
|
|
280
|
+
const deliveredHash = (await lastDeliveredBlock.hash()).toString();
|
|
281
|
+
if (deliveredHash !== sourceTips.proposed.hash) {
|
|
282
|
+
this.log.warn(`Delivered proposed-block hash differs from snapshot; skipping reconciliation this pass`, {
|
|
283
|
+
blockNumber: lastDeliveredBlock.number,
|
|
284
|
+
deliveredHash,
|
|
285
|
+
snapshotHash: sourceTips.proposed.hash,
|
|
286
|
+
});
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
295
292
|
/**
|
|
296
293
|
* Returns whether the source and local agree on the block hash at a given height.
|
|
297
294
|
* @param blockNumber - The block number to test.
|
|
298
|
-
* @param args - A cache of data already requested from source
|
|
295
|
+
* @param args - A cache of data already requested from source (to avoid re-requesting it) and the source's
|
|
296
|
+
* advertised proposed tip from the pass snapshot (to detect an incoherent source).
|
|
299
297
|
*/
|
|
300
|
-
private async areBlockHashesEqualAt(
|
|
298
|
+
private async areBlockHashesEqualAt(
|
|
299
|
+
blockNumber: BlockNumber,
|
|
300
|
+
args: { sourceCache: BlockHashCache; sourceProposed: BlockNumber },
|
|
301
|
+
) {
|
|
301
302
|
const localBlockHash = await this.localData.getL2BlockHash(blockNumber);
|
|
302
303
|
if (!localBlockHash && this.opts.skipFinalized) {
|
|
303
304
|
// Failing to find a block hash when skipping finalized blocks can be highly problematic as we'd potentially need
|
|
@@ -307,12 +308,28 @@ export class L2BlockStream {
|
|
|
307
308
|
this.log.error(`No local block hash for block number ${blockNumber}`);
|
|
308
309
|
throw new AbortError();
|
|
309
310
|
}
|
|
311
|
+
if (!localBlockHash) {
|
|
312
|
+
// A missing local hash compares UNEQUAL: treating both-undefined as equal would stop the walk above the true
|
|
313
|
+
// divergence (an under-deep prune no later pass re-detects). Over-deep is the safe direction, and block 0 always
|
|
314
|
+
// resolves via the store's initialBlockHash so the walk always terminates.
|
|
315
|
+
this.log.trace(`No local block hash for block number ${blockNumber}; treating as unequal`);
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
310
318
|
|
|
311
319
|
const sourceBlockHashFromCache = args.sourceCache.get(blockNumber);
|
|
312
320
|
const sourceBlockHash = args.sourceCache.get(blockNumber) ?? (await this.getBlockHashFromSource(blockNumber));
|
|
313
321
|
if (!sourceBlockHashFromCache && sourceBlockHash) {
|
|
314
322
|
args.sourceCache.add({ number: blockNumber, hash: sourceBlockHash });
|
|
315
323
|
}
|
|
324
|
+
if (!sourceBlockHash && blockNumber !== 0 && blockNumber <= args.sourceProposed) {
|
|
325
|
+
// No source data at or below the source's own proposed tip: the source contradicts itself (mid-reorg unwind or
|
|
326
|
+
// a transient read failure), so skip this pass.
|
|
327
|
+
this.log.warn(`Source has no data for a block at or below its proposed tip; skipping this sync pass`, {
|
|
328
|
+
blockNumber,
|
|
329
|
+
sourceProposed: args.sourceProposed,
|
|
330
|
+
});
|
|
331
|
+
throw new AbortError();
|
|
332
|
+
}
|
|
316
333
|
|
|
317
334
|
this.log.trace(`Comparing block hashes for block ${blockNumber}`, { localBlockHash, sourceBlockHash });
|
|
318
335
|
return localBlockHash === sourceBlockHash;
|
|
@@ -327,7 +344,13 @@ export class L2BlockStream {
|
|
|
327
344
|
|
|
328
345
|
private async emitEvent(event: L2BlockStreamEvent) {
|
|
329
346
|
this.log.debug(
|
|
330
|
-
`Emitting ${event.type} (${
|
|
347
|
+
`Emitting ${event.type} (${
|
|
348
|
+
event.type === 'blocks-added'
|
|
349
|
+
? event.blocks.length
|
|
350
|
+
: event.type === 'chain-checkpointed'
|
|
351
|
+
? event.checkpoint.number
|
|
352
|
+
: event.block.number
|
|
353
|
+
})`,
|
|
331
354
|
);
|
|
332
355
|
await this.handler.handleBlockStreamEvent(event);
|
|
333
356
|
if (!this.isRunning() && !this.isSyncing) {
|
|
@@ -76,11 +76,30 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Records `(number → hash)` witnesses into the block-hash index without moving any tip cursor. In tips-only mode
|
|
81
|
+
* the hash history is sparse (one anchor per tip-moving poll), so a consumer that materializes per-height state
|
|
82
|
+
* should witness those heights or its prunes are over-deep by the gap to the nearest anchor. Witnesses are compared
|
|
83
|
+
* against the source, not trusted, so they cannot cause under-deep prunes; this is always safe to call.
|
|
84
|
+
*/
|
|
85
|
+
public async recordBlockHashes(blocks: L2BlockId[]): Promise<void> {
|
|
86
|
+
await this.runInTransaction(async () => {
|
|
87
|
+
for (const block of blocks) {
|
|
88
|
+
if (block.hash) {
|
|
89
|
+
await this.setBlockHash(block.number, block.hash);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
80
96
|
switch (event.type) {
|
|
81
97
|
case 'blocks-added':
|
|
82
98
|
await this.handleBlocksAdded(event);
|
|
83
99
|
break;
|
|
100
|
+
case 'chain-proposed':
|
|
101
|
+
await this.handleChainProposed(event);
|
|
102
|
+
break;
|
|
84
103
|
case 'chain-checkpointed':
|
|
85
104
|
await this.handleChainCheckpointed(event);
|
|
86
105
|
break;
|
|
@@ -148,17 +167,22 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl
|
|
|
148
167
|
});
|
|
149
168
|
}
|
|
150
169
|
|
|
170
|
+
private async handleChainProposed(event: L2BlockStreamEvent): Promise<void> {
|
|
171
|
+
if (event.type !== 'chain-proposed') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Records the proposed tip into the block-hash index the walk-back reads. In tips-only mode this is the sole
|
|
175
|
+
// writer of proposed-tip history, leaving one sparse anchor per tip-moving pass for reorg detection.
|
|
176
|
+
await this.runInTransaction(() => this.saveTag('proposed', event.block));
|
|
177
|
+
}
|
|
178
|
+
|
|
151
179
|
private async handleChainCheckpointed(event: L2BlockStreamEvent): Promise<void> {
|
|
152
180
|
if (event.type !== 'chain-checkpointed') {
|
|
153
181
|
return;
|
|
154
182
|
}
|
|
155
183
|
await this.runInTransaction(async () => {
|
|
156
|
-
const checkpointId: CheckpointId = {
|
|
157
|
-
number: event.checkpoint.checkpoint.number,
|
|
158
|
-
hash: event.checkpoint.checkpoint.hash().toString(),
|
|
159
|
-
};
|
|
160
184
|
await this.saveTag('checkpointed', event.block);
|
|
161
|
-
await this.setTipCheckpoint('checkpointed',
|
|
185
|
+
await this.setTipCheckpoint('checkpointed', event.checkpoint);
|
|
162
186
|
});
|
|
163
187
|
}
|
|
164
188
|
|
|
@@ -167,18 +191,15 @@ export abstract class L2TipsStoreBase implements L2BlockStreamEventHandler, L2Bl
|
|
|
167
191
|
return;
|
|
168
192
|
}
|
|
169
193
|
await this.runInTransaction(async () => {
|
|
170
|
-
// A prune is a rollback: the proposed tip moves to the prune target unconditionally, but
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// would then throw on.
|
|
194
|
+
// A prune is a rollback: the proposed tip moves to the prune target unconditionally, but checkpoint-bearing
|
|
195
|
+
// cursors may only move backward (advancing one onto an uncheckpointed block would leave it without a recorded
|
|
196
|
+
// checkpoint id, which getCheckpointId throws on).
|
|
174
197
|
await this.saveTag('proposed', event.block);
|
|
175
198
|
|
|
176
|
-
// Clamp each checkpoint-bearing cursor down to its OWN source tip when it leads it
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
// boundary, so the clamped cursor always resolves to a recorded id. The source guarantees proven <=
|
|
181
|
-
// checkpointed, so clamping each cursor to its own tip preserves the local proven <= checkpointed invariant.
|
|
199
|
+
// Clamp each checkpoint-bearing cursor down to its OWN source tip when it leads it; the event carries a valid
|
|
200
|
+
// (block, id) pair for each. Clamping the proven cursor onto the (possibly higher) checkpointed tip instead
|
|
201
|
+
// would transiently report unproven blocks as proven. The source guarantees proven <= checkpointed, so per-tip
|
|
202
|
+
// clamping preserves the local invariant.
|
|
182
203
|
for (const { tag, sourceTip } of [
|
|
183
204
|
{ tag: 'checkpointed', sourceTip: event.checkpointed },
|
|
184
205
|
{ tag: 'proven', sourceTip: event.proven },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
|
|
2
|
-
import { hexToBuffer } from '@aztec/foundation/string';
|
|
2
|
+
import { bufferToHex, hexToBuffer } from '@aztec/foundation/string';
|
|
3
3
|
|
|
4
4
|
import { encodeAbiParameters, parseAbiParameters } from 'viem';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -76,8 +76,10 @@ export class CommitteeAttestationsAndSigners implements Signable {
|
|
|
76
76
|
let totalDataSize = 0;
|
|
77
77
|
for (const attestation of viemAttestations) {
|
|
78
78
|
const signature = attestation.signature;
|
|
79
|
-
//
|
|
80
|
-
|
|
79
|
+
// A slot is empty (a non-signing member, packed as its address) only when r, s and v are all zero
|
|
80
|
+
// — matching Signature.isEmpty() and getSigners(), so the bitmap popcount and the signers list can
|
|
81
|
+
// never disagree (which would revert propose() with SignersSizeMismatch).
|
|
82
|
+
const isEmpty = signature.v === 0 && BigInt(signature.r) === 0n && BigInt(signature.s) === 0n;
|
|
81
83
|
|
|
82
84
|
if (!isEmpty) {
|
|
83
85
|
totalDataSize += 65; // v (1) + r (32) + s (32)
|
|
@@ -93,8 +95,8 @@ export class CommitteeAttestationsAndSigners implements Signable {
|
|
|
93
95
|
for (const [i, attestation] of viemAttestations.entries()) {
|
|
94
96
|
const signature = attestation.signature;
|
|
95
97
|
|
|
96
|
-
//
|
|
97
|
-
const isEmpty = signature.v === 0;
|
|
98
|
+
// Empty iff r, s and v are all zero (see the size-tally loop above).
|
|
99
|
+
const isEmpty = signature.v === 0 && BigInt(signature.r) === 0n && BigInt(signature.s) === 0n;
|
|
98
100
|
|
|
99
101
|
if (!isEmpty) {
|
|
100
102
|
// Set bit in bitmap (bit 7-0 in each byte, left to right)
|
|
@@ -102,8 +104,10 @@ export class CommitteeAttestationsAndSigners implements Signable {
|
|
|
102
104
|
const bitIndex = 7 - (i % 8);
|
|
103
105
|
signatureIndices[byteIndex] = (signatureIndices[byteIndex] ?? 0) | (1 << bitIndex);
|
|
104
106
|
|
|
105
|
-
// Pack signature: v + r + s
|
|
106
|
-
|
|
107
|
+
// Pack signature: v + r + s. Canonicalize a yParity recovery byte (v = 0/1) to 27/28 — it
|
|
108
|
+
// recovers to the same signer, but L1 ECDSA.recover only accepts 27/28. Any other value is left
|
|
109
|
+
// as-is so a genuinely malformed signature still fails on L1 rather than being silently rewritten.
|
|
110
|
+
signaturesOrAddresses[dataIndex] = signature.v === 0 || signature.v === 1 ? signature.v + 27 : signature.v;
|
|
107
111
|
dataIndex++;
|
|
108
112
|
|
|
109
113
|
// Pack r (32 bytes)
|
|
@@ -153,3 +157,45 @@ export class MaliciousCommitteeAttestationsAndSigners extends CommitteeAttestati
|
|
|
153
157
|
return this.signers;
|
|
154
158
|
}
|
|
155
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Malicious extension of CommitteeAttestationsAndSigners that rewrites every non-proposer signature slot's
|
|
163
|
+
* recovery byte to yParity form (v ∈ {0, 1}) in the packed output, after the honest `packAttestations` has
|
|
164
|
+
* already canonicalized it to v ∈ {27, 28}. Models a malicious selected proposer that hand-crafts
|
|
165
|
+
* `propose()` calldata L1 accepts but no honest node can byte-replay: each rewritten signature still
|
|
166
|
+
* recovers to the same member (r, s and the recovery parity are preserved), the bitmap bits stay set, and
|
|
167
|
+
* `getSigners()` stays consistent, so `propose()` does not revert `SignersSizeMismatch` -- yet the
|
|
168
|
+
* checkpoint can never be proven (`ECDSA.recover` rejects v ∉ {27, 28}). The proposer's own slot is left
|
|
169
|
+
* canonical so L1 `verifyProposer` (which recovers that slot) still accepts the checkpoint. For testing only.
|
|
170
|
+
*/
|
|
171
|
+
export class MaliciousYParityCommitteeAttestationsAndSigners extends CommitteeAttestationsAndSigners {
|
|
172
|
+
constructor(
|
|
173
|
+
attestations: CommitteeAttestation[],
|
|
174
|
+
/** Committee index of the proposer's own slot, left canonical so `propose()` passes `verifyProposer`. */
|
|
175
|
+
private proposerIndex: number,
|
|
176
|
+
signatureContext: CoordinationSignatureContext,
|
|
177
|
+
) {
|
|
178
|
+
super(attestations, signatureContext);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
override getPackedAttestations(): ViemCommitteeAttestations {
|
|
182
|
+
const packed = super.getPackedAttestations();
|
|
183
|
+
const data = hexToBuffer(packed.signaturesOrAddresses);
|
|
184
|
+
|
|
185
|
+
// Walk the packed byte-vector and rewrite every non-proposer signed slot's v-byte to yParity form. A
|
|
186
|
+
// signed slot occupies 65 bytes (v, r, s); an empty slot occupies 20 bytes (address only).
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (let i = 0; i < this.attestations.length; i++) {
|
|
189
|
+
const isSigned = !this.attestations[i].signature.isEmpty();
|
|
190
|
+
if (isSigned && i !== this.proposerIndex) {
|
|
191
|
+
// `packAttestations` canonicalized v to 27/28; rewrite back to the equivalent yParity byte (0/1),
|
|
192
|
+
// preserving the recovery parity so the signature still recovers to the same member.
|
|
193
|
+
const v = data[offset];
|
|
194
|
+
data[offset] = v >= 27 ? v - 27 : v;
|
|
195
|
+
}
|
|
196
|
+
offset += isSigned ? 65 : 20;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { signatureIndices: packed.signatureIndices, signaturesOrAddresses: bufferToHex(data) };
|
|
200
|
+
}
|
|
201
|
+
}
|