@aztec-labs/archiver 6.0.0-rc.1 → 6.0.0-rc.2
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/README.md +7 -1
- package/dest/errors.d.ts +20 -1
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +22 -0
- package/dest/l1/bin/retrieve-calldata.js +14 -4
- package/dest/l1/calldata_retriever.d.ts +15 -17
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +2 -9
- package/dest/l1/data_retrieval.d.ts +7 -3
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +14 -9
- package/dest/modules/data_store_updater.js +5 -5
- package/dest/modules/l1_synchronizer.d.ts +1 -1
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +228 -191
- package/dest/modules/validation.d.ts +14 -8
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +72 -22
- package/dest/store/block_store.d.ts +2 -1
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +34 -14
- package/dest/store/contract_class_store.d.ts +1 -1
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +4 -2
- package/dest/store/contract_instance_store.d.ts +5 -4
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +20 -9
- package/dest/test/fake_l1_state.d.ts +3 -1
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +4 -4
- package/package.json +13 -13
- package/src/errors.ts +32 -0
- package/src/l1/bin/retrieve-calldata.ts +15 -4
- package/src/l1/calldata_retriever.ts +14 -20
- package/src/l1/data_retrieval.ts +22 -14
- package/src/modules/data_store_updater.ts +4 -4
- package/src/modules/l1_synchronizer.ts +294 -261
- package/src/modules/validation.ts +87 -28
- package/src/store/block_store.ts +41 -13
- package/src/store/contract_class_store.ts +4 -4
- package/src/store/contract_instance_store.ts +24 -17
- package/src/test/fake_l1_state.ts +9 -3
|
@@ -3,10 +3,11 @@ import type { ViemCommitteeAttestations } from '@aztec-labs/ethereum/contracts';
|
|
|
3
3
|
import { type CheckpointNumber, EpochNumber } from '@aztec-labs/foundation/branded-types';
|
|
4
4
|
import { compactArray } from '@aztec-labs/foundation/collection';
|
|
5
5
|
import type { Fr } from '@aztec-labs/foundation/curves/bn254';
|
|
6
|
+
import type { EthAddress } from '@aztec-labs/foundation/eth-address';
|
|
6
7
|
import type { Logger } from '@aztec-labs/foundation/log';
|
|
7
8
|
import {
|
|
8
9
|
type AttestationInfo,
|
|
9
|
-
|
|
10
|
+
CommitteeAttestation,
|
|
10
11
|
type ValidateCheckpointNegativeResult,
|
|
11
12
|
type ValidateCheckpointResult,
|
|
12
13
|
getAttestationInfoFromPayload,
|
|
@@ -16,6 +17,8 @@ import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec-la
|
|
|
16
17
|
import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec-labs/stdlib/p2p';
|
|
17
18
|
import type { CheckpointHeader } from '@aztec-labs/stdlib/rollup';
|
|
18
19
|
|
|
20
|
+
import { CheckpointAttestationsDecodeError } from '../errors.js';
|
|
21
|
+
|
|
19
22
|
export type { ValidateCheckpointResult };
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -36,24 +39,57 @@ export type CalldataCheckpointForAttestations = {
|
|
|
36
39
|
archiveRoot: Fr;
|
|
37
40
|
feeAssetPriceModifier: bigint;
|
|
38
41
|
header: CheckpointHeader;
|
|
39
|
-
attestations: CommitteeAttestation[];
|
|
40
42
|
/** The exact packed attestations tuple from L1 calldata, carried verbatim for byte-faithful invalidation. */
|
|
41
43
|
verbatimAttestations: ViemCommitteeAttestations;
|
|
42
44
|
};
|
|
43
45
|
|
|
46
|
+
/** A checkpoint's attestations decoded for its epoch committee, and their validation result. */
|
|
47
|
+
export type ResolvedCheckpointAttestations = {
|
|
48
|
+
/** Empty if the epoch has an open escape hatch or no committee. */
|
|
49
|
+
attestations: CommitteeAttestation[];
|
|
50
|
+
validationResult: ValidateCheckpointResult;
|
|
51
|
+
};
|
|
52
|
+
|
|
44
53
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
54
|
+
* Decodes and validates the attestations of a checkpoint from L1 calldata only, so an invalid checkpoint can be
|
|
55
|
+
* rejected before its blobs are fetched. The tuple is decoded against the epoch's committee size, and not at all
|
|
56
|
+
* during an escape hatch, where the proposer may post an arbitrary tuple.
|
|
57
|
+
*
|
|
58
|
+
* @throws CheckpointAttestationsDecodeError if the tuple does not decode for the epoch committee.
|
|
49
59
|
*/
|
|
50
|
-
export function
|
|
60
|
+
export async function resolveCheckpointAttestationsFromCalldata(
|
|
51
61
|
checkpoint: CalldataCheckpointForAttestations,
|
|
52
62
|
epochCache: EpochCache,
|
|
53
63
|
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
54
64
|
signatureContext: CoordinationSignatureContext,
|
|
55
65
|
logger?: Logger,
|
|
56
|
-
): Promise<
|
|
66
|
+
): Promise<ResolvedCheckpointAttestations> {
|
|
67
|
+
const slot = checkpoint.header.slotNumber;
|
|
68
|
+
const epoch: EpochNumber = getEpochAtSlot(slot, constants);
|
|
69
|
+
// Read the hatch flag from the same cache entry as the committee, so both come from one snapshot.
|
|
70
|
+
const { committee, seed, isEscapeHatchOpen } = await epochCache.getCommitteeForEpoch(epoch);
|
|
71
|
+
|
|
72
|
+
if (isEscapeHatchOpen) {
|
|
73
|
+
logger?.warn(
|
|
74
|
+
`Escape hatch open for epoch ${epoch} at slot ${slot}, skipping attestations of checkpoint ${checkpoint.checkpointNumber}`,
|
|
75
|
+
);
|
|
76
|
+
return { attestations: [], validationResult: { valid: true } };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!committee || committee.length === 0) {
|
|
80
|
+
logger?.warn(
|
|
81
|
+
`No committee found for epoch ${epoch} at slot ${slot}. Accepting checkpoint ${checkpoint.checkpointNumber} without validation.`,
|
|
82
|
+
);
|
|
83
|
+
return { attestations: [], validationResult: { valid: true } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let attestations: CommitteeAttestation[];
|
|
87
|
+
try {
|
|
88
|
+
attestations = CommitteeAttestation.fromPacked(checkpoint.verbatimAttestations, committee.length);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
throw new CheckpointAttestationsDecodeError(checkpoint.checkpointNumber, epoch, committee.length, err);
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
const payload = new ConsensusPayload(
|
|
58
94
|
checkpoint.header,
|
|
59
95
|
checkpoint.archiveRoot,
|
|
@@ -67,15 +103,16 @@ export function validateCheckpointAttestationsFromCalldata(
|
|
|
67
103
|
checkpointNumber: checkpoint.checkpointNumber,
|
|
68
104
|
timestamp: checkpoint.header.timestamp,
|
|
69
105
|
};
|
|
70
|
-
|
|
106
|
+
|
|
107
|
+
const validationResult = validateAttestationsAgainstCommittee(
|
|
71
108
|
payload,
|
|
72
|
-
|
|
109
|
+
attestations,
|
|
73
110
|
checkpoint.verbatimAttestations,
|
|
74
111
|
checkpointInfo,
|
|
75
|
-
|
|
76
|
-
constants,
|
|
112
|
+
{ committee, seed, epoch },
|
|
77
113
|
logger,
|
|
78
114
|
);
|
|
115
|
+
return { attestations, validationResult };
|
|
79
116
|
}
|
|
80
117
|
|
|
81
118
|
/**
|
|
@@ -92,36 +129,58 @@ export async function validateAttestations(
|
|
|
92
129
|
constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
93
130
|
logger?: Logger,
|
|
94
131
|
): Promise<ValidateCheckpointResult> {
|
|
132
|
+
const slot = payload.header.slotNumber;
|
|
133
|
+
const epoch: EpochNumber = getEpochAtSlot(slot, constants);
|
|
134
|
+
const { committee, seed, isEscapeHatchOpen } = await epochCache.getCommitteeForEpoch(epoch);
|
|
135
|
+
|
|
136
|
+
if (isEscapeHatchOpen) {
|
|
137
|
+
logger?.warn(`Escape hatch open for epoch ${epoch} at slot ${slot}, skipping checkpoint validation`);
|
|
138
|
+
return { valid: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!committee || committee.length === 0) {
|
|
142
|
+
logger?.warn(`No committee found for epoch ${epoch} at slot ${slot}. Accepting checkpoint without validation.`, {
|
|
143
|
+
checkpointNumber: checkpointInfo.checkpointNumber,
|
|
144
|
+
slot,
|
|
145
|
+
epoch,
|
|
146
|
+
});
|
|
147
|
+
return { valid: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return validateAttestationsAgainstCommittee(
|
|
151
|
+
payload,
|
|
152
|
+
attestations,
|
|
153
|
+
verbatimAttestations,
|
|
154
|
+
checkpointInfo,
|
|
155
|
+
{ committee, seed, epoch },
|
|
156
|
+
logger,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Validates decoded attestations against a known, non-empty epoch committee. */
|
|
161
|
+
function validateAttestationsAgainstCommittee(
|
|
162
|
+
payload: ConsensusPayload,
|
|
163
|
+
attestations: CommitteeAttestation[],
|
|
164
|
+
verbatimAttestations: ViemCommitteeAttestations,
|
|
165
|
+
checkpointInfo: CheckpointInfo,
|
|
166
|
+
{ committee, seed, epoch }: { committee: EthAddress[]; seed: bigint; epoch: EpochNumber },
|
|
167
|
+
logger?: Logger,
|
|
168
|
+
): ValidateCheckpointResult {
|
|
95
169
|
const attestorInfos = getAttestationInfoFromPayload(payload, attestations);
|
|
96
170
|
const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
|
|
97
171
|
const headerHash = payload.header.hash();
|
|
98
172
|
const archiveRoot = payload.archive.toString();
|
|
99
173
|
const slot = payload.header.slotNumber;
|
|
100
174
|
const checkpointNumber = checkpointInfo.checkpointNumber;
|
|
101
|
-
const epoch: EpochNumber = getEpochAtSlot(slot, constants);
|
|
102
|
-
const { committee, seed } = await epochCache.getCommitteeForEpoch(epoch);
|
|
103
175
|
const logData = { checkpointNumber, slot, epoch, headerHash, archiveRoot };
|
|
104
176
|
|
|
105
177
|
logger?.debug(`Validating attestations for checkpoint ${checkpointNumber} at slot ${slot} in epoch ${epoch}`, {
|
|
106
|
-
committee:
|
|
178
|
+
committee: committee.map(member => member.toString()),
|
|
107
179
|
recoveredAttestors: attestorInfos,
|
|
108
180
|
postedAttestations: attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
|
|
109
181
|
...logData,
|
|
110
182
|
});
|
|
111
183
|
|
|
112
|
-
if (!committee || committee.length === 0) {
|
|
113
|
-
logger?.warn(
|
|
114
|
-
`No committee found for epoch ${epoch} at slot ${slot}. Accepting checkpoint without validation.`,
|
|
115
|
-
logData,
|
|
116
|
-
);
|
|
117
|
-
return { valid: true };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (await epochCache.isEscapeHatchOpen(epoch)) {
|
|
121
|
-
logger?.warn(`Escape hatch open for epoch ${epoch} at slot ${slot}, skipping checkpoint validation`);
|
|
122
|
-
return { valid: true };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
184
|
const requiredAttestationCount = computeQuorum(committee.length);
|
|
126
185
|
|
|
127
186
|
const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
|
package/src/store/block_store.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
CannotOverwriteCheckpointedBlockError,
|
|
52
52
|
CheckpointNotFoundError,
|
|
53
53
|
CheckpointNumberNotSequentialError,
|
|
54
|
+
DuplicateTxHashError,
|
|
54
55
|
InitialCheckpointNumberNotSequentialError,
|
|
55
56
|
NoProposedCheckpointToPromoteError,
|
|
56
57
|
ProposedCheckpointArchiveRootMismatchError,
|
|
@@ -569,6 +570,10 @@ export class BlockStore {
|
|
|
569
570
|
indexWithinCheckpoint,
|
|
570
571
|
});
|
|
571
572
|
|
|
573
|
+
await this.assertTxsNotInAncestors(
|
|
574
|
+
block.body.txEffects.map(tx => tx.txHash),
|
|
575
|
+
block.number,
|
|
576
|
+
);
|
|
572
577
|
for (let i = 0; i < block.body.txEffects.length; i++) {
|
|
573
578
|
const txEffect: IndexedTxEffect = {
|
|
574
579
|
data: block.body.txEffects[i],
|
|
@@ -587,6 +592,28 @@ export class BlockStore {
|
|
|
587
592
|
await this.#blockArchiveIndex.set(block.archive.root.toString(), block.number);
|
|
588
593
|
}
|
|
589
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Throws if any of the txs is already owned by a stored block below `blockNumber`. Stored blocks below the one being
|
|
597
|
+
* inserted are its ancestors (archive chaining is enforced on insert), so the tx would be included twice and the new
|
|
598
|
+
* block would take over the ancestor's tx effect entry. An owner at the same or a higher height is a block being
|
|
599
|
+
* replaced (a losing local proposal, a re-presented checkpoint), so its entry is overwritten. This relies on replaced
|
|
600
|
+
* blocks at lower heights having been removed via deleteBlock first (as the updater does before inserting
|
|
601
|
+
* checkpoints), which also deletes their tx effect entries.
|
|
602
|
+
*/
|
|
603
|
+
private async assertTxsNotInAncestors(txHashes: TxHash[], blockNumber: BlockNumber): Promise<void> {
|
|
604
|
+
const existing = await this.#txEffects.getManyAsync(txHashes.map(txHash => txHash.toString()));
|
|
605
|
+
for (let i = 0; i < txHashes.length; i++) {
|
|
606
|
+
const stored = existing[i];
|
|
607
|
+
if (stored === undefined) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const owner = readTxLocation(stored).blockNumber;
|
|
611
|
+
if (owner < blockNumber) {
|
|
612
|
+
throw new DuplicateTxHashError(txHashes[i], blockNumber, owner);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
590
617
|
/** Deletes a block and all associated data (tx effects, indices). */
|
|
591
618
|
private async deleteBlock(blockNumber: number, blockStorage: BlockStorage): Promise<void> {
|
|
592
619
|
// Delete the block from the main blocks map
|
|
@@ -1283,19 +1310,7 @@ export class BlockStore {
|
|
|
1283
1310
|
*/
|
|
1284
1311
|
public async getTxLocation(txHash: TxHash): Promise<TxLocation | undefined> {
|
|
1285
1312
|
const txEffect = await this.#txEffects.getAsync(txHash.toString());
|
|
1286
|
-
|
|
1287
|
-
return undefined;
|
|
1288
|
-
}
|
|
1289
|
-
// Read only the IndexedTxEffect header (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4)`); the
|
|
1290
|
-
// large tail (the full TxEffect with logs etc.) is irrelevant here.
|
|
1291
|
-
const view = Buffer.from(txEffect.buffer, txEffect.byteOffset, txEffect.byteLength);
|
|
1292
|
-
const l2BlockNumber = view.readUInt32BE(32);
|
|
1293
|
-
const txIndexInBlock = view.readUInt32BE(36);
|
|
1294
|
-
return {
|
|
1295
|
-
blockNumber: BlockNumber(l2BlockNumber),
|
|
1296
|
-
blockHash: BlockHash.fromBuffer(view.subarray(0, 32)),
|
|
1297
|
-
txIndexInBlock,
|
|
1298
|
-
};
|
|
1313
|
+
return txEffect ? readTxLocation(txEffect) : undefined;
|
|
1299
1314
|
}
|
|
1300
1315
|
|
|
1301
1316
|
/**
|
|
@@ -1714,3 +1729,16 @@ export class BlockStore {
|
|
|
1714
1729
|
};
|
|
1715
1730
|
}
|
|
1716
1731
|
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Reads only the header of a serialized IndexedTxEffect (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4)`),
|
|
1735
|
+
* skipping the large tail (the full TxEffect with logs etc.).
|
|
1736
|
+
*/
|
|
1737
|
+
function readTxLocation(txEffect: Buffer): TxLocation {
|
|
1738
|
+
const view = Buffer.from(txEffect.buffer, txEffect.byteOffset, txEffect.byteLength);
|
|
1739
|
+
return {
|
|
1740
|
+
blockNumber: BlockNumber(view.readUInt32BE(32)),
|
|
1741
|
+
blockHash: BlockHash.fromBuffer(view.subarray(0, 32)),
|
|
1742
|
+
txIndexInBlock: view.readUInt32BE(36),
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
@@ -28,9 +28,8 @@ export class ContractClassStore {
|
|
|
28
28
|
* @returns True if every insert succeeded.
|
|
29
29
|
*/
|
|
30
30
|
async addContractClasses(data: ContractClassPublicWithCommitment[], blockNumber: number): Promise<boolean> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
);
|
|
31
|
+
await Promise.all(data.map(c => this.addContractClass(c, c.publicBytecodeCommitment, blockNumber)));
|
|
32
|
+
return true;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -40,7 +39,8 @@ export class ContractClassStore {
|
|
|
40
39
|
* @returns True if every delete succeeded.
|
|
41
40
|
*/
|
|
42
41
|
async deleteContractClasses(data: ContractClassPublic[], blockNumber: number): Promise<boolean> {
|
|
43
|
-
|
|
42
|
+
await Promise.all(data.map(c => this.deleteContractClass(c, blockNumber)));
|
|
43
|
+
return true;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async addContractClass(
|
|
@@ -45,16 +45,19 @@ export class ContractInstanceStore {
|
|
|
45
45
|
* @returns True if every insert succeeded.
|
|
46
46
|
*/
|
|
47
47
|
async addContractInstances(data: ContractInstanceWithAddress[], blockNumber: number): Promise<boolean> {
|
|
48
|
-
|
|
48
|
+
await Promise.all(data.map(c => this.addContractInstance(c, blockNumber)));
|
|
49
|
+
return true;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* Removes multiple contract instances from the store.
|
|
53
|
+
* Removes multiple contract instances from the store, but only those that were published at the given block.
|
|
53
54
|
* @param data - Contract instances to delete.
|
|
55
|
+
* @param blockNumber - L2 block being unwound, which must be the one that published each deleted instance.
|
|
54
56
|
* @returns True if every delete succeeded.
|
|
55
57
|
*/
|
|
56
|
-
async deleteContractInstances(data: ContractInstanceWithAddress[]): Promise<boolean> {
|
|
57
|
-
|
|
58
|
+
async deleteContractInstances(data: ContractInstanceWithAddress[], blockNumber: number): Promise<boolean> {
|
|
59
|
+
await Promise.all(data.map(c => this.deleteContractInstance(c, blockNumber)));
|
|
60
|
+
return true;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -69,11 +72,10 @@ export class ContractInstanceStore {
|
|
|
69
72
|
timestamp: UInt64,
|
|
70
73
|
blockNumber: BlockNumber,
|
|
71
74
|
): Promise<boolean> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
).every(Boolean);
|
|
75
|
+
await Promise.all(
|
|
76
|
+
data.map((update, logIndex) => this.addContractInstanceUpdate(update, timestamp, blockNumber, logIndex)),
|
|
77
|
+
);
|
|
78
|
+
return true;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
/**
|
|
@@ -88,11 +90,10 @@ export class ContractInstanceStore {
|
|
|
88
90
|
timestamp: UInt64,
|
|
89
91
|
blockNumber: BlockNumber,
|
|
90
92
|
): Promise<boolean> {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
).every(Boolean);
|
|
93
|
+
await Promise.all(
|
|
94
|
+
data.map((update, logIndex) => this.deleteContractInstanceUpdate(update, timestamp, blockNumber, logIndex)),
|
|
95
|
+
);
|
|
96
|
+
return true;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
addContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise<void> {
|
|
@@ -119,15 +120,21 @@ export class ContractInstanceStore {
|
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
deleteContractInstance(contractInstance: ContractInstanceWithAddress): Promise<void> {
|
|
123
|
+
deleteContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise<void> {
|
|
123
124
|
// Protocol contracts are preloaded at block 0 and must never be deleted, even when the block that
|
|
124
125
|
// (re-)published them on-chain is unwound by a reorg.
|
|
125
126
|
if (isProtocolContract(contractInstance.address)) {
|
|
126
127
|
return Promise.resolve();
|
|
127
128
|
}
|
|
128
129
|
return this.db.transactionAsync(async () => {
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
const key = contractInstance.address.toString();
|
|
131
|
+
// An unwound block can carry a log naming an address first published by a surviving block (a forged log in an
|
|
132
|
+
// unexecuted checkpoint, which the store path skipped). Deleting by address alone would erase that instance.
|
|
133
|
+
if ((await this.#contractInstancePublishedAt.getAsync(key)) !== blockNumber) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await this.#contractInstances.delete(key);
|
|
137
|
+
await this.#contractInstancePublishedAt.delete(key);
|
|
131
138
|
});
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -80,6 +80,8 @@ type AddCheckpointOptions = {
|
|
|
80
80
|
* `attestationsHash` the rollup stores at propose time covers.
|
|
81
81
|
*/
|
|
82
82
|
spareAttestationsBitmapBit?: boolean;
|
|
83
|
+
/** Overrides the posted attestations tuple, e.g. to model an escape-hatch proposer's arbitrary tuple. */
|
|
84
|
+
verbatimAttestations?: ViemCommitteeAttestations;
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
/** Result from adding a checkpoint. */
|
|
@@ -263,6 +265,7 @@ export class FakeL1State {
|
|
|
263
265
|
checkpoint,
|
|
264
266
|
signers,
|
|
265
267
|
options.spareAttestationsBitmapBit ?? false,
|
|
268
|
+
options.verbatimAttestations,
|
|
266
269
|
);
|
|
267
270
|
const blobHashes = await this.makeVersionedBlobHashes(checkpoint);
|
|
268
271
|
const blobs = await this.makeBlobsFromCheckpoint(checkpoint);
|
|
@@ -776,6 +779,7 @@ export class FakeL1State {
|
|
|
776
779
|
checkpoint: Checkpoint,
|
|
777
780
|
signers: Secp256k1Signer[],
|
|
778
781
|
spareAttestationsBitmapBit: boolean,
|
|
782
|
+
verbatimAttestationsOverride?: ViemCommitteeAttestations,
|
|
779
783
|
): Promise<{
|
|
780
784
|
tx: Transaction;
|
|
781
785
|
attestationsHash: Buffer32;
|
|
@@ -806,9 +810,11 @@ export class FakeL1State {
|
|
|
806
810
|
);
|
|
807
811
|
|
|
808
812
|
const packedAttestations = attestationsAndSigners.getPackedAttestations();
|
|
809
|
-
const verbatimAttestations =
|
|
810
|
-
|
|
811
|
-
|
|
813
|
+
const verbatimAttestations =
|
|
814
|
+
verbatimAttestationsOverride ??
|
|
815
|
+
(spareAttestationsBitmapBit
|
|
816
|
+
? { ...packedAttestations, signatureIndices: setLastBitmapBit(packedAttestations.signatureIndices) }
|
|
817
|
+
: packedAttestations);
|
|
812
818
|
|
|
813
819
|
const rollupInput = encodeFunctionData({
|
|
814
820
|
abi: RollupAbi,
|