@aztec/archiver 3.0.0-nightly.20251210 → 3.0.0-nightly.20251211
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/archiver/archiver.d.ts +9 -6
- package/dest/archiver/archiver.d.ts.map +1 -1
- package/dest/archiver/archiver.js +28 -10
- package/dest/archiver/archiver_store.d.ts +3 -3
- package/dest/archiver/archiver_store.d.ts.map +1 -1
- package/dest/archiver/config.d.ts +3 -2
- package/dest/archiver/config.d.ts.map +1 -1
- package/dest/archiver/config.js +8 -1
- package/dest/archiver/instrumentation.d.ts +3 -1
- package/dest/archiver/instrumentation.d.ts.map +1 -1
- package/dest/archiver/instrumentation.js +11 -0
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +3 -3
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/message_store.d.ts +2 -2
- package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
- package/dest/archiver/l1/bin/retrieve-calldata.d.ts +3 -0
- package/dest/archiver/l1/bin/retrieve-calldata.d.ts.map +1 -0
- package/dest/archiver/l1/bin/retrieve-calldata.js +147 -0
- package/dest/archiver/l1/calldata_retriever.d.ts +98 -0
- package/dest/archiver/l1/calldata_retriever.d.ts.map +1 -0
- package/dest/archiver/l1/calldata_retriever.js +403 -0
- package/dest/archiver/l1/data_retrieval.d.ts +87 -0
- package/dest/archiver/l1/data_retrieval.d.ts.map +1 -0
- package/dest/archiver/{data_retrieval.js → l1/data_retrieval.js} +19 -89
- package/dest/archiver/l1/debug_tx.d.ts +19 -0
- package/dest/archiver/l1/debug_tx.d.ts.map +1 -0
- package/dest/archiver/l1/debug_tx.js +73 -0
- package/dest/archiver/l1/spire_proposer.d.ts +70 -0
- package/dest/archiver/l1/spire_proposer.d.ts.map +1 -0
- package/dest/archiver/l1/spire_proposer.js +157 -0
- package/dest/archiver/l1/trace_tx.d.ts +97 -0
- package/dest/archiver/l1/trace_tx.d.ts.map +1 -0
- package/dest/archiver/l1/trace_tx.js +91 -0
- package/dest/archiver/l1/types.d.ts +12 -0
- package/dest/archiver/l1/types.d.ts.map +1 -0
- package/dest/archiver/l1/types.js +3 -0
- package/dest/archiver/l1/validate_trace.d.ts +29 -0
- package/dest/archiver/l1/validate_trace.d.ts.map +1 -0
- package/dest/archiver/l1/validate_trace.js +150 -0
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/test/mock_l2_block_source.js +1 -1
- package/package.json +15 -14
- package/src/archiver/archiver.ts +45 -20
- package/src/archiver/archiver_store.ts +2 -2
- package/src/archiver/config.ts +8 -7
- package/src/archiver/instrumentation.ts +14 -0
- package/src/archiver/kv_archiver_store/kv_archiver_store.ts +2 -2
- package/src/archiver/kv_archiver_store/message_store.ts +1 -1
- package/src/archiver/l1/README.md +98 -0
- package/src/archiver/l1/bin/retrieve-calldata.ts +182 -0
- package/src/archiver/l1/calldata_retriever.ts +531 -0
- package/src/archiver/{data_retrieval.ts → l1/data_retrieval.ts} +50 -137
- package/src/archiver/l1/debug_tx.ts +99 -0
- package/src/archiver/l1/spire_proposer.ts +160 -0
- package/src/archiver/l1/trace_tx.ts +128 -0
- package/src/archiver/l1/types.ts +13 -0
- package/src/archiver/l1/validate_trace.ts +211 -0
- package/src/index.ts +1 -1
- package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
- package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
- package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
- package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
- package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
- package/src/test/fixtures/trace_transaction-proxied.json +128 -0
- package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
- package/src/test/mock_l2_block_source.ts +1 -1
- package/dest/archiver/data_retrieval.d.ts +0 -80
- package/dest/archiver/data_retrieval.d.ts.map +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { createPublicClient, http } from 'viem';
|
|
6
|
+
import { mainnet } from 'viem/chains';
|
|
7
|
+
import { CalldataRetriever } from '../calldata_retriever.js';
|
|
8
|
+
const logger = createLogger('archiver:calldata-test');
|
|
9
|
+
function parseArgs() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
if (args.length < 2) {
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.error('Usage: node index.js <rollup-address> <tx-hash> [target-committee-size]');
|
|
14
|
+
// eslint-disable-next-line no-console
|
|
15
|
+
console.error('');
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.error('Environment variables:');
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.error(' ETHEREUM_HOST or RPC_URL - Ethereum RPC endpoint');
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.error('');
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error('Example:');
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.error(' RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR-API-KEY \\');
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.error(' node index.js 0x1234... 0xabcd... 32');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const rollupAddress = EthAddress.fromString(args[0]);
|
|
31
|
+
const txHash = args[1];
|
|
32
|
+
const targetCommitteeSize = args[2] ? parseInt(args[2], 10) : 24;
|
|
33
|
+
const rpcUrl = process.env.ETHEREUM_HOST || process.env.RPC_URL;
|
|
34
|
+
if (!rpcUrl) {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.error('Error: ETHEREUM_HOST or RPC_URL environment variable must be set');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
if (targetCommitteeSize <= 0 || targetCommitteeSize > 256) {
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.error('Error: target-committee-size must be between 1 and 256');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
rollupAddress,
|
|
46
|
+
txHash,
|
|
47
|
+
rpcUrl,
|
|
48
|
+
targetCommitteeSize
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function main() {
|
|
52
|
+
const { rollupAddress, txHash, rpcUrl, targetCommitteeSize } = parseArgs();
|
|
53
|
+
logger.info('Calldata Retriever Test Script');
|
|
54
|
+
logger.info('===============================');
|
|
55
|
+
logger.info(`Rollup Address: ${rollupAddress.toString()}`);
|
|
56
|
+
logger.info(`Transaction Hash: ${txHash}`);
|
|
57
|
+
logger.info(`RPC URL: ${rpcUrl}`);
|
|
58
|
+
logger.info(`Target Committee Size: ${targetCommitteeSize}`);
|
|
59
|
+
logger.info('');
|
|
60
|
+
try {
|
|
61
|
+
// Create viem public client
|
|
62
|
+
const publicClient = createPublicClient({
|
|
63
|
+
chain: mainnet,
|
|
64
|
+
transport: http(rpcUrl)
|
|
65
|
+
});
|
|
66
|
+
logger.info('Fetching transaction...');
|
|
67
|
+
const tx = await publicClient.getTransaction({
|
|
68
|
+
hash: txHash
|
|
69
|
+
});
|
|
70
|
+
if (!tx) {
|
|
71
|
+
throw new Error(`Transaction ${txHash} not found`);
|
|
72
|
+
}
|
|
73
|
+
logger.info(`Transaction found in block ${tx.blockNumber}`);
|
|
74
|
+
// For simplicity, use zero addresses for optional contract addresses
|
|
75
|
+
// In production, these would be fetched from the rollup contract or configuration
|
|
76
|
+
const slashingProposerAddress = EthAddress.ZERO;
|
|
77
|
+
const governanceProposerAddress = EthAddress.ZERO;
|
|
78
|
+
const slashFactoryAddress = undefined;
|
|
79
|
+
logger.info('Using zero addresses for governance/slashing (can be configured if needed)');
|
|
80
|
+
// Create CalldataRetriever
|
|
81
|
+
const retriever = new CalldataRetriever(publicClient, publicClient, targetCommitteeSize, undefined, logger, {
|
|
82
|
+
rollupAddress,
|
|
83
|
+
governanceProposerAddress,
|
|
84
|
+
slashingProposerAddress,
|
|
85
|
+
slashFactoryAddress
|
|
86
|
+
});
|
|
87
|
+
// Extract L2 block number from transaction logs
|
|
88
|
+
logger.info('Decoding transaction to extract L2 block number...');
|
|
89
|
+
const receipt = await publicClient.getTransactionReceipt({
|
|
90
|
+
hash: txHash
|
|
91
|
+
});
|
|
92
|
+
const l2BlockProposedEvent = receipt.logs.find((log)=>{
|
|
93
|
+
try {
|
|
94
|
+
// Try to match the L2BlockProposed event
|
|
95
|
+
return log.address.toLowerCase() === rollupAddress.toString().toLowerCase() && log.topics[0] === '0x2f1d0e696fa5186494a2f2f89a0e0bcbb15d607f6c5eac4637e07e1e5e7d3c00' // L2BlockProposed event signature
|
|
96
|
+
;
|
|
97
|
+
} catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
let l2BlockNumber;
|
|
102
|
+
if (l2BlockProposedEvent && l2BlockProposedEvent.topics[1]) {
|
|
103
|
+
// L2 block number is typically the first indexed parameter
|
|
104
|
+
l2BlockNumber = Number(BigInt(l2BlockProposedEvent.topics[1]));
|
|
105
|
+
logger.info(`L2 Block Number (from event): ${l2BlockNumber}`);
|
|
106
|
+
} else {
|
|
107
|
+
// Fallback: try to extract from transaction data or use a default
|
|
108
|
+
logger.warn('Could not extract L2 block number from event, using block number as fallback');
|
|
109
|
+
l2BlockNumber = Number(tx.blockNumber);
|
|
110
|
+
}
|
|
111
|
+
logger.info('');
|
|
112
|
+
logger.info('Retrieving block header from rollup transaction...');
|
|
113
|
+
logger.info('');
|
|
114
|
+
// For this script, we don't have blob hashes, so pass empty array
|
|
115
|
+
const result = await retriever.getCheckpointFromRollupTx(txHash, [], CheckpointNumber(l2BlockNumber));
|
|
116
|
+
logger.info(' Successfully retrieved block header!');
|
|
117
|
+
logger.info('');
|
|
118
|
+
logger.info('Block Header Details:');
|
|
119
|
+
logger.info('====================');
|
|
120
|
+
logger.info(`Checkpoint Number: ${result.checkpointNumber}`);
|
|
121
|
+
logger.info(`Block Hash: ${result.blockHash}`);
|
|
122
|
+
logger.info(`Archive Root: ${result.archiveRoot.toString()}`);
|
|
123
|
+
logger.info('');
|
|
124
|
+
logger.info('Header:');
|
|
125
|
+
logger.info(` Slot Number: ${result.header.slotNumber.toString()}`);
|
|
126
|
+
logger.info(` Timestamp: ${result.header.timestamp.toString()}`);
|
|
127
|
+
logger.info(` Coinbase: ${result.header.coinbase.toString()}`);
|
|
128
|
+
logger.info(` Fee Recipient: ${result.header.feeRecipient.toString()}`);
|
|
129
|
+
logger.info(` Total Mana Used: ${result.header.totalManaUsed.toString()}`);
|
|
130
|
+
logger.info('');
|
|
131
|
+
logger.info('Attestations:');
|
|
132
|
+
logger.info(` Count: ${result.attestations.length}`);
|
|
133
|
+
logger.info(` Non-empty attestations: ${result.attestations.filter((a)=>!a.signature.isEmpty()).length}`);
|
|
134
|
+
process.exit(0);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.error('Error retrieving block header:');
|
|
137
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
138
|
+
if (error instanceof Error && error.stack) {
|
|
139
|
+
logger.debug(error.stack);
|
|
140
|
+
}
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Only run if this is the main module
|
|
145
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
146
|
+
void main();
|
|
147
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
|
|
2
|
+
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
|
+
import type { Logger } from '@aztec/foundation/log';
|
|
6
|
+
import { CommitteeAttestation } from '@aztec/stdlib/block';
|
|
7
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
8
|
+
import { type Hex, type Transaction } from 'viem';
|
|
9
|
+
import type { ArchiverInstrumentation } from '../instrumentation.js';
|
|
10
|
+
/**
|
|
11
|
+
* Extracts calldata to the `propose` method of the rollup contract from an L1 transaction
|
|
12
|
+
* in order to reconstruct an L2 block header.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CalldataRetriever {
|
|
15
|
+
private readonly publicClient;
|
|
16
|
+
private readonly debugClient;
|
|
17
|
+
private readonly targetCommitteeSize;
|
|
18
|
+
private readonly instrumentation;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
/** Pre-computed valid contract calls for validation */
|
|
21
|
+
private readonly validContractCalls;
|
|
22
|
+
private readonly rollupAddress;
|
|
23
|
+
constructor(publicClient: ViemPublicClient, debugClient: ViemPublicDebugClient, targetCommitteeSize: number, instrumentation: ArchiverInstrumentation | undefined, logger: Logger, contractAddresses: {
|
|
24
|
+
rollupAddress: EthAddress;
|
|
25
|
+
governanceProposerAddress: EthAddress;
|
|
26
|
+
slashingProposerAddress: EthAddress;
|
|
27
|
+
slashFactoryAddress?: EthAddress;
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Gets checkpoint header and metadata from the calldata of an L1 transaction.
|
|
31
|
+
* Tries multicall3 decoding, falls back to trace-based extraction.
|
|
32
|
+
* @param txHash - Hash of the tx that published it.
|
|
33
|
+
* @param blobHashes - Blob hashes for the checkpoint.
|
|
34
|
+
* @param checkpointNumber - Checkpoint number.
|
|
35
|
+
* @returns Checkpoint header and metadata from the calldata, deserialized
|
|
36
|
+
*/
|
|
37
|
+
getCheckpointFromRollupTx(txHash: `0x${string}`, blobHashes: Buffer[], checkpointNumber: CheckpointNumber): Promise<{
|
|
38
|
+
checkpointNumber: CheckpointNumber;
|
|
39
|
+
archiveRoot: Fr;
|
|
40
|
+
header: CheckpointHeader;
|
|
41
|
+
attestations: CommitteeAttestation[];
|
|
42
|
+
blockHash: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Gets rollup propose calldata from a transaction */
|
|
45
|
+
protected getProposeCallData(tx: Transaction, checkpointNumber: CheckpointNumber): Promise<Hex>;
|
|
46
|
+
/**
|
|
47
|
+
* Attempts to decode a transaction as a Spire Proposer multicall wrapper.
|
|
48
|
+
* If successful, extracts the wrapped call and validates it as either multicall3 or direct propose.
|
|
49
|
+
* @param tx - The transaction to decode
|
|
50
|
+
* @returns The propose calldata if successfully decoded and validated, undefined otherwise
|
|
51
|
+
*/
|
|
52
|
+
protected tryDecodeSpireProposer(tx: Transaction): Promise<Hex | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* Attempts to decode transaction input as multicall3 and extract propose calldata.
|
|
55
|
+
* Returns undefined if validation fails.
|
|
56
|
+
* @param tx - The transaction-like object with to, input, and hash
|
|
57
|
+
* @returns The propose calldata if successfully validated, undefined otherwise
|
|
58
|
+
*/
|
|
59
|
+
protected tryDecodeMulticall3(tx: {
|
|
60
|
+
to: Hex | null | undefined;
|
|
61
|
+
input: Hex;
|
|
62
|
+
hash: Hex;
|
|
63
|
+
}): Hex | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Attempts to decode transaction as a direct propose call to the rollup contract.
|
|
66
|
+
* Returns undefined if validation fails.
|
|
67
|
+
* @param tx - The transaction-like object with to, input, and hash
|
|
68
|
+
* @returns The propose calldata if successfully validated, undefined otherwise
|
|
69
|
+
*/
|
|
70
|
+
protected tryDecodeDirectPropose(tx: {
|
|
71
|
+
to: Hex | null | undefined;
|
|
72
|
+
input: Hex;
|
|
73
|
+
hash: Hex;
|
|
74
|
+
}): Hex | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Uses debug/trace RPC to extract the actual calldata from the successful propose call.
|
|
77
|
+
* This is the definitive fallback that works for any transaction pattern.
|
|
78
|
+
* Tries trace_transaction first, then falls back to debug_traceTransaction.
|
|
79
|
+
* @param txHash - The transaction hash to trace
|
|
80
|
+
* @returns The propose calldata from the successful call
|
|
81
|
+
*/
|
|
82
|
+
protected extractCalldataViaTrace(txHash: Hex): Promise<Hex>;
|
|
83
|
+
/**
|
|
84
|
+
* Decodes propose calldata and builds the checkpoint header structure.
|
|
85
|
+
* @param proposeCalldata - The propose function calldata
|
|
86
|
+
* @param blockHash - The L1 block hash containing this transaction
|
|
87
|
+
* @param checkpointNumber - The checkpoint number
|
|
88
|
+
* @returns The decoded checkpoint header and metadata
|
|
89
|
+
*/
|
|
90
|
+
protected decodeAndBuildCheckpoint(proposeCalldata: Hex, blockHash: Hex, checkpointNumber: CheckpointNumber): {
|
|
91
|
+
checkpointNumber: CheckpointNumber;
|
|
92
|
+
archiveRoot: Fr;
|
|
93
|
+
header: CheckpointHeader;
|
|
94
|
+
attestations: CommitteeAttestation[];
|
|
95
|
+
blockHash: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FsbGRhdGFfcmV0cmlldmVyLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvYXJjaGl2ZXIvbDEvY2FsbGRhdGFfcmV0cmlldmVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLGdCQUFnQixFQUFFLHFCQUFxQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDckYsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbkUsT0FBTyxFQUFFLEVBQUUsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBQ3BELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUUzRCxPQUFPLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQVFwRCxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUMzRCxPQUFPLEVBQUUsZ0JBQWdCLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUV4RCxPQUFPLEVBQUUsS0FBSyxHQUFHLEVBQUUsS0FBSyxXQUFXLEVBQXFFLE1BQU0sTUFBTSxDQUFDO0FBRXJILE9BQU8sS0FBSyxFQUFFLHVCQUF1QixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFNckU7OztHQUdHO0FBQ0gscUJBQWEsaUJBQWlCO0lBTzFCLE9BQU8sQ0FBQyxRQUFRLENBQUMsWUFBWTtJQUM3QixPQUFPLENBQUMsUUFBUSxDQUFDLFdBQVc7SUFDNUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxtQkFBbUI7SUFDcEMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxlQUFlO0lBQ2hDLE9BQU8sQ0FBQyxRQUFRLENBQUMsTUFBTTtJQVZ6Qix1REFBdUQ7SUFDdkQsT0FBTyxDQUFDLFFBQVEsQ0FBQyxrQkFBa0IsQ0FBc0I7SUFFekQsT0FBTyxDQUFDLFFBQVEsQ0FBQyxhQUFhLENBQWE7SUFFM0MsWUFDbUIsWUFBWSxFQUFFLGdCQUFnQixFQUM5QixXQUFXLEVBQUUscUJBQXFCLEVBQ2xDLG1CQUFtQixFQUFFLE1BQU0sRUFDM0IsZUFBZSxFQUFFLHVCQUF1QixHQUFHLFNBQVMsRUFDcEQsTUFBTSxFQUFFLE1BQU0sRUFDL0IsaUJBQWlCLEVBQUU7UUFDakIsYUFBYSxFQUFFLFVBQVUsQ0FBQztRQUMxQix5QkFBeUIsRUFBRSxVQUFVLENBQUM7UUFDdEMsdUJBQXVCLEVBQUUsVUFBVSxDQUFDO1FBQ3BDLG1CQUFtQixDQUFDLEVBQUUsVUFBVSxDQUFDO0tBQ2xDLEVBSUY7SUFFRDs7Ozs7OztPQU9HO0lBQ0cseUJBQXlCLENBQzdCLE1BQU0sRUFBRSxLQUFLLE1BQU0sRUFBRSxFQUNyQixVQUFVLEVBQUUsTUFBTSxFQUFFLEVBQ3BCLGdCQUFnQixFQUFFLGdCQUFnQixHQUNqQyxPQUFPLENBQUM7UUFDVCxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztRQUNuQyxXQUFXLEVBQUUsRUFBRSxDQUFDO1FBQ2hCLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztRQUN6QixZQUFZLEVBQUUsb0JBQW9CLEVBQUUsQ0FBQztRQUNyQyxTQUFTLEVBQUUsTUFBTSxDQUFDO0tBQ25CLENBQUMsQ0FLRDtJQUVELHNEQUFzRDtJQUN0RCxVQUFnQixrQkFBa0IsQ0FBQyxFQUFFLEVBQUUsV0FBVyxFQUFFLGdCQUFnQixFQUFFLGdCQUFnQixHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0ErQnBHO0lBRUQ7Ozs7O09BS0c7SUFDSCxVQUFnQixzQkFBc0IsQ0FBQyxFQUFFLEVBQUUsV0FBVyxHQUFHLE9BQU8sQ0FBQyxHQUFHLEdBQUcsU0FBUyxDQUFDLENBNEJoRjtJQUVEOzs7OztPQUtHO0lBQ0gsU0FBUyxDQUFDLG1CQUFtQixDQUFDLEVBQUUsRUFBRTtRQUFFLEVBQUUsRUFBRSxHQUFHLEdBQUcsSUFBSSxHQUFHLFNBQVMsQ0FBQztRQUFDLEtBQUssRUFBRSxHQUFHLENBQUM7UUFBQyxJQUFJLEVBQUUsR0FBRyxDQUFBO0tBQUUsR0FBRyxHQUFHLEdBQUcsU0FBUyxDQXVGeEc7SUFFRDs7Ozs7T0FLRztJQUNILFNBQVMsQ0FBQyxzQkFBc0IsQ0FBQyxFQUFFLEVBQUU7UUFBRSxFQUFFLEVBQUUsR0FBRyxHQUFHLElBQUksR0FBRyxTQUFTLENBQUM7UUFBQyxLQUFLLEVBQUUsR0FBRyxDQUFDO1FBQUMsSUFBSSxFQUFFLEdBQUcsQ0FBQTtLQUFFLEdBQUcsR0FBRyxHQUFHLFNBQVMsQ0EwQjNHO0lBRUQ7Ozs7OztPQU1HO0lBQ0gsVUFBZ0IsdUJBQXVCLENBQUMsTUFBTSxFQUFFLEdBQUcsR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLENBeUNqRTtJQUVEOzs7Ozs7T0FNRztJQUNILFNBQVMsQ0FBQyx3QkFBd0IsQ0FDaEMsZUFBZSxFQUFFLEdBQUcsRUFDcEIsU0FBUyxFQUFFLEdBQUcsRUFDZCxnQkFBZ0IsRUFBRSxnQkFBZ0IsR0FDakM7UUFDRCxnQkFBZ0IsRUFBRSxnQkFBZ0IsQ0FBQztRQUNuQyxXQUFXLEVBQUUsRUFBRSxDQUFDO1FBQ2hCLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQztRQUN6QixZQUFZLEVBQUUsb0JBQW9CLEVBQUUsQ0FBQztRQUNyQyxTQUFTLEVBQUUsTUFBTSxDQUFDO0tBQ25CLENBNkNBO0NBQ0YifQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calldata_retriever.d.ts","sourceRoot":"","sources":["../../../src/archiver/l1/calldata_retriever.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts';
|
|
2
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { EmpireSlashingProposerAbi, GovernanceProposerAbi, RollupAbi, SlashFactoryAbi, TallySlashingProposerAbi } from '@aztec/l1-artifacts';
|
|
5
|
+
import { CommitteeAttestation } from '@aztec/stdlib/block';
|
|
6
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
7
|
+
import { decodeFunctionData, hexToBytes, multicall3Abi, toFunctionSelector } from 'viem';
|
|
8
|
+
import { getSuccessfulCallsFromDebug } from './debug_tx.js';
|
|
9
|
+
import { getCallFromSpireProposer } from './spire_proposer.js';
|
|
10
|
+
import { getSuccessfulCallsFromTrace } from './trace_tx.js';
|
|
11
|
+
/**
|
|
12
|
+
* Extracts calldata to the `propose` method of the rollup contract from an L1 transaction
|
|
13
|
+
* in order to reconstruct an L2 block header.
|
|
14
|
+
*/ export class CalldataRetriever {
|
|
15
|
+
publicClient;
|
|
16
|
+
debugClient;
|
|
17
|
+
targetCommitteeSize;
|
|
18
|
+
instrumentation;
|
|
19
|
+
logger;
|
|
20
|
+
/** Pre-computed valid contract calls for validation */ validContractCalls;
|
|
21
|
+
rollupAddress;
|
|
22
|
+
constructor(publicClient, debugClient, targetCommitteeSize, instrumentation, logger, contractAddresses){
|
|
23
|
+
this.publicClient = publicClient;
|
|
24
|
+
this.debugClient = debugClient;
|
|
25
|
+
this.targetCommitteeSize = targetCommitteeSize;
|
|
26
|
+
this.instrumentation = instrumentation;
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.rollupAddress = contractAddresses.rollupAddress;
|
|
29
|
+
this.validContractCalls = computeValidContractCalls(contractAddresses);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Gets checkpoint header and metadata from the calldata of an L1 transaction.
|
|
33
|
+
* Tries multicall3 decoding, falls back to trace-based extraction.
|
|
34
|
+
* @param txHash - Hash of the tx that published it.
|
|
35
|
+
* @param blobHashes - Blob hashes for the checkpoint.
|
|
36
|
+
* @param checkpointNumber - Checkpoint number.
|
|
37
|
+
* @returns Checkpoint header and metadata from the calldata, deserialized
|
|
38
|
+
*/ async getCheckpointFromRollupTx(txHash, blobHashes, checkpointNumber) {
|
|
39
|
+
this.logger.trace(`Fetching checkpoint ${checkpointNumber} from rollup tx ${txHash}`);
|
|
40
|
+
const tx = await this.publicClient.getTransaction({
|
|
41
|
+
hash: txHash
|
|
42
|
+
});
|
|
43
|
+
const proposeCalldata = await this.getProposeCallData(tx, checkpointNumber);
|
|
44
|
+
return this.decodeAndBuildCheckpoint(proposeCalldata, tx.blockHash, checkpointNumber);
|
|
45
|
+
}
|
|
46
|
+
/** Gets rollup propose calldata from a transaction */ async getProposeCallData(tx, checkpointNumber) {
|
|
47
|
+
// Try to decode as multicall3 with validation
|
|
48
|
+
const proposeCalldata = this.tryDecodeMulticall3(tx);
|
|
49
|
+
if (proposeCalldata) {
|
|
50
|
+
this.logger.trace(`Decoded propose calldata from multicall3 for tx ${tx.hash}`);
|
|
51
|
+
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
52
|
+
return proposeCalldata;
|
|
53
|
+
}
|
|
54
|
+
// Try to decode as direct propose call
|
|
55
|
+
const directProposeCalldata = this.tryDecodeDirectPropose(tx);
|
|
56
|
+
if (directProposeCalldata) {
|
|
57
|
+
this.logger.trace(`Decoded propose calldata from direct call for tx ${tx.hash}`);
|
|
58
|
+
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
59
|
+
return directProposeCalldata;
|
|
60
|
+
}
|
|
61
|
+
// Try to decode as Spire Proposer multicall wrapper
|
|
62
|
+
const spireProposeCalldata = await this.tryDecodeSpireProposer(tx);
|
|
63
|
+
if (spireProposeCalldata) {
|
|
64
|
+
this.logger.trace(`Decoded propose calldata from Spire Proposer for tx ${tx.hash}`);
|
|
65
|
+
this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
|
|
66
|
+
return spireProposeCalldata;
|
|
67
|
+
}
|
|
68
|
+
// Fall back to trace-based extraction
|
|
69
|
+
this.logger.warn(`Failed to decode multicall3, direct propose, or Spire proposer for L1 tx ${tx.hash}, falling back to trace for checkpoint ${checkpointNumber}`);
|
|
70
|
+
this.instrumentation?.recordBlockProposalTxTarget(tx.to ?? EthAddress.ZERO.toString(), true);
|
|
71
|
+
return await this.extractCalldataViaTrace(tx.hash);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Attempts to decode a transaction as a Spire Proposer multicall wrapper.
|
|
75
|
+
* If successful, extracts the wrapped call and validates it as either multicall3 or direct propose.
|
|
76
|
+
* @param tx - The transaction to decode
|
|
77
|
+
* @returns The propose calldata if successfully decoded and validated, undefined otherwise
|
|
78
|
+
*/ async tryDecodeSpireProposer(tx) {
|
|
79
|
+
// Try to decode as Spire Proposer multicall (extracts the wrapped call)
|
|
80
|
+
const spireWrappedCall = await getCallFromSpireProposer(tx, this.publicClient, this.logger);
|
|
81
|
+
if (!spireWrappedCall) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
this.logger.trace(`Decoded Spire Proposer wrapping for tx ${tx.hash}, inner call to ${spireWrappedCall.to}`);
|
|
85
|
+
// Now try to decode the wrapped call as either multicall3 or direct propose
|
|
86
|
+
const wrappedTx = {
|
|
87
|
+
to: spireWrappedCall.to,
|
|
88
|
+
input: spireWrappedCall.data,
|
|
89
|
+
hash: tx.hash
|
|
90
|
+
};
|
|
91
|
+
const multicall3Calldata = this.tryDecodeMulticall3(wrappedTx);
|
|
92
|
+
if (multicall3Calldata) {
|
|
93
|
+
this.logger.trace(`Decoded propose calldata from Spire Proposer to multicall3 for tx ${tx.hash}`);
|
|
94
|
+
return multicall3Calldata;
|
|
95
|
+
}
|
|
96
|
+
const directProposeCalldata = this.tryDecodeDirectPropose(wrappedTx);
|
|
97
|
+
if (directProposeCalldata) {
|
|
98
|
+
this.logger.trace(`Decoded propose calldata from Spire Proposer to direct propose for tx ${tx.hash}`);
|
|
99
|
+
return directProposeCalldata;
|
|
100
|
+
}
|
|
101
|
+
this.logger.warn(`Spire Proposer wrapped call could not be decoded as multicall3 or direct propose for tx ${tx.hash}`);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Attempts to decode transaction input as multicall3 and extract propose calldata.
|
|
106
|
+
* Returns undefined if validation fails.
|
|
107
|
+
* @param tx - The transaction-like object with to, input, and hash
|
|
108
|
+
* @returns The propose calldata if successfully validated, undefined otherwise
|
|
109
|
+
*/ tryDecodeMulticall3(tx) {
|
|
110
|
+
const txHash = tx.hash;
|
|
111
|
+
try {
|
|
112
|
+
// Check if transaction is to Multicall3 address
|
|
113
|
+
if (!tx.to || !EthAddress.areEqual(tx.to, MULTI_CALL_3_ADDRESS)) {
|
|
114
|
+
this.logger.debug(`Transaction is not to Multicall3 address (to: ${tx.to})`, {
|
|
115
|
+
txHash,
|
|
116
|
+
to: tx.to
|
|
117
|
+
});
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
// Try to decode as multicall3 aggregate3 call
|
|
121
|
+
const { functionName: multicall3Fn, args: multicall3Args } = decodeFunctionData({
|
|
122
|
+
abi: multicall3Abi,
|
|
123
|
+
data: tx.input
|
|
124
|
+
});
|
|
125
|
+
// If not aggregate3, return undefined (not a multicall3 transaction)
|
|
126
|
+
if (multicall3Fn !== 'aggregate3') {
|
|
127
|
+
this.logger.warn(`Transaction is not multicall3 aggregate3 (got ${multicall3Fn})`, {
|
|
128
|
+
txHash
|
|
129
|
+
});
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
if (multicall3Args.length !== 1) {
|
|
133
|
+
this.logger.warn(`Unexpected number of arguments for multicall3 (got ${multicall3Args.length})`, {
|
|
134
|
+
txHash
|
|
135
|
+
});
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const [calls] = multicall3Args;
|
|
139
|
+
// Validate all calls and find propose calls
|
|
140
|
+
const rollupAddressLower = this.rollupAddress.toString().toLowerCase();
|
|
141
|
+
const proposeCalls = [];
|
|
142
|
+
for(let i = 0; i < calls.length; i++){
|
|
143
|
+
const addr = calls[i].target.toLowerCase();
|
|
144
|
+
const callData = calls[i].callData;
|
|
145
|
+
// Extract function selector (first 4 bytes)
|
|
146
|
+
if (callData.length < 10) {
|
|
147
|
+
// "0x" + 8 hex chars = 10 chars minimum for a valid function call
|
|
148
|
+
this.logger.warn(`Invalid calldata length at index ${i} (${callData.length})`, {
|
|
149
|
+
txHash
|
|
150
|
+
});
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
const functionSelector = callData.slice(0, 10);
|
|
154
|
+
// Validate this call is allowed by searching through valid calls
|
|
155
|
+
const validCall = this.validContractCalls.find((vc)=>vc.address === addr && vc.functionSelector === functionSelector);
|
|
156
|
+
if (!validCall) {
|
|
157
|
+
this.logger.warn(`Invalid contract call detected in multicall3`, {
|
|
158
|
+
index: i,
|
|
159
|
+
targetAddress: addr,
|
|
160
|
+
functionSelector,
|
|
161
|
+
validCalls: this.validContractCalls.map((c)=>({
|
|
162
|
+
address: c.address,
|
|
163
|
+
selector: c.functionSelector
|
|
164
|
+
})),
|
|
165
|
+
txHash
|
|
166
|
+
});
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
this.logger.trace(`Valid call found to ${addr}`, {
|
|
170
|
+
validCall
|
|
171
|
+
});
|
|
172
|
+
// Collect propose calls specifically
|
|
173
|
+
if (addr === rollupAddressLower && validCall.functionName === 'propose') {
|
|
174
|
+
proposeCalls.push(callData);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Validate exactly ONE propose call
|
|
178
|
+
if (proposeCalls.length === 0) {
|
|
179
|
+
this.logger.warn(`No propose calls found in multicall3`, {
|
|
180
|
+
txHash
|
|
181
|
+
});
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
if (proposeCalls.length > 1) {
|
|
185
|
+
this.logger.warn(`Multiple propose calls found in multicall3 (${proposeCalls.length})`, {
|
|
186
|
+
txHash
|
|
187
|
+
});
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
// Successfully extracted single propose call
|
|
191
|
+
return proposeCalls[0];
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// Any decoding error triggers fallback to trace
|
|
194
|
+
this.logger.warn(`Failed to decode multicall3: ${err}`, {
|
|
195
|
+
txHash
|
|
196
|
+
});
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Attempts to decode transaction as a direct propose call to the rollup contract.
|
|
202
|
+
* Returns undefined if validation fails.
|
|
203
|
+
* @param tx - The transaction-like object with to, input, and hash
|
|
204
|
+
* @returns The propose calldata if successfully validated, undefined otherwise
|
|
205
|
+
*/ tryDecodeDirectPropose(tx) {
|
|
206
|
+
const txHash = tx.hash;
|
|
207
|
+
try {
|
|
208
|
+
// Check if transaction is to the rollup address
|
|
209
|
+
if (!tx.to || !EthAddress.areEqual(tx.to, this.rollupAddress)) {
|
|
210
|
+
this.logger.debug(`Transaction is not to rollup address (to: ${tx.to})`, {
|
|
211
|
+
txHash
|
|
212
|
+
});
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
// Try to decode as propose call
|
|
216
|
+
const { functionName } = decodeFunctionData({
|
|
217
|
+
abi: RollupAbi,
|
|
218
|
+
data: tx.input
|
|
219
|
+
});
|
|
220
|
+
// If not propose, return undefined
|
|
221
|
+
if (functionName !== 'propose') {
|
|
222
|
+
this.logger.warn(`Transaction to rollup is not propose (got ${functionName})`, {
|
|
223
|
+
txHash
|
|
224
|
+
});
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
// Successfully validated direct propose call
|
|
228
|
+
this.logger.trace(`Validated direct propose call to rollup`, {
|
|
229
|
+
txHash
|
|
230
|
+
});
|
|
231
|
+
return tx.input;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Any decoding error means it's not a valid propose call
|
|
234
|
+
this.logger.warn(`Failed to decode as direct propose: ${err}`, {
|
|
235
|
+
txHash
|
|
236
|
+
});
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Uses debug/trace RPC to extract the actual calldata from the successful propose call.
|
|
242
|
+
* This is the definitive fallback that works for any transaction pattern.
|
|
243
|
+
* Tries trace_transaction first, then falls back to debug_traceTransaction.
|
|
244
|
+
* @param txHash - The transaction hash to trace
|
|
245
|
+
* @returns The propose calldata from the successful call
|
|
246
|
+
*/ async extractCalldataViaTrace(txHash) {
|
|
247
|
+
const rollupAddress = this.rollupAddress;
|
|
248
|
+
const selector = PROPOSE_SELECTOR;
|
|
249
|
+
let calls;
|
|
250
|
+
try {
|
|
251
|
+
// Try trace_transaction first (using Parity/OpenEthereum/Erigon RPC)
|
|
252
|
+
this.logger.debug(`Attempting to trace transaction ${txHash} using trace_transaction`);
|
|
253
|
+
calls = await getSuccessfulCallsFromTrace(this.debugClient, txHash, rollupAddress, selector, this.logger);
|
|
254
|
+
this.logger.debug(`Successfully traced using trace_transaction, found ${calls.length} calls`);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const traceError = err instanceof Error ? err : new Error(String(err));
|
|
257
|
+
this.logger.verbose(`Failed trace_transaction for ${txHash}`, {
|
|
258
|
+
traceError
|
|
259
|
+
});
|
|
260
|
+
try {
|
|
261
|
+
// Fall back to debug_traceTransaction (Geth RPC)
|
|
262
|
+
this.logger.debug(`Attempting to trace transaction ${txHash} using debug_traceTransaction`);
|
|
263
|
+
calls = await getSuccessfulCallsFromDebug(this.debugClient, txHash, rollupAddress, selector, this.logger);
|
|
264
|
+
this.logger.debug(`Successfully traced using debug_traceTransaction, found ${calls.length} calls`);
|
|
265
|
+
} catch (debugErr) {
|
|
266
|
+
const debugError = debugErr instanceof Error ? debugErr : new Error(String(debugErr));
|
|
267
|
+
this.logger.warn(`All tracing methods failed for tx ${txHash}`, {
|
|
268
|
+
traceError,
|
|
269
|
+
debugError,
|
|
270
|
+
txHash
|
|
271
|
+
});
|
|
272
|
+
throw new Error(`Failed to trace transaction ${txHash} to extract propose calldata`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Validate exactly ONE successful propose call
|
|
276
|
+
if (calls.length === 0) {
|
|
277
|
+
throw new Error(`No successful propose calls found in transaction ${txHash}`);
|
|
278
|
+
}
|
|
279
|
+
if (calls.length > 1) {
|
|
280
|
+
throw new Error(`Multiple successful propose calls found in transaction ${txHash} (${calls.length})`);
|
|
281
|
+
}
|
|
282
|
+
// Return the calldata from the single successful propose call
|
|
283
|
+
return calls[0].input;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Decodes propose calldata and builds the checkpoint header structure.
|
|
287
|
+
* @param proposeCalldata - The propose function calldata
|
|
288
|
+
* @param blockHash - The L1 block hash containing this transaction
|
|
289
|
+
* @param checkpointNumber - The checkpoint number
|
|
290
|
+
* @returns The decoded checkpoint header and metadata
|
|
291
|
+
*/ decodeAndBuildCheckpoint(proposeCalldata, blockHash, checkpointNumber) {
|
|
292
|
+
const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
|
|
293
|
+
abi: RollupAbi,
|
|
294
|
+
data: proposeCalldata
|
|
295
|
+
});
|
|
296
|
+
if (rollupFunctionName !== 'propose') {
|
|
297
|
+
throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
|
|
298
|
+
}
|
|
299
|
+
const [decodedArgs, packedAttestations, _signers, _attestationsAndSignersSignature, _blobInput] = rollupArgs;
|
|
300
|
+
const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
|
|
301
|
+
this.logger.trace(`Decoded propose calldata`, {
|
|
302
|
+
checkpointNumber,
|
|
303
|
+
archive: decodedArgs.archive,
|
|
304
|
+
header: decodedArgs.header,
|
|
305
|
+
l1BlockHash: blockHash,
|
|
306
|
+
attestations,
|
|
307
|
+
packedAttestations,
|
|
308
|
+
targetCommitteeSize: this.targetCommitteeSize
|
|
309
|
+
});
|
|
310
|
+
const header = CheckpointHeader.fromViem(decodedArgs.header);
|
|
311
|
+
const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
|
|
312
|
+
return {
|
|
313
|
+
checkpointNumber,
|
|
314
|
+
archiveRoot,
|
|
315
|
+
header,
|
|
316
|
+
attestations,
|
|
317
|
+
blockHash
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Pre-computed function selectors for all valid contract calls.
|
|
323
|
+
* These are computed once at module load time from the ABIs.
|
|
324
|
+
* Based on analysis of sequencer-client/src/publisher/sequencer-publisher.ts
|
|
325
|
+
*/ // Rollup contract function selectors (always valid)
|
|
326
|
+
const PROPOSE_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'propose'));
|
|
327
|
+
const INVALIDATE_BAD_ATTESTATION_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateBadAttestation'));
|
|
328
|
+
const INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateInsufficientAttestations'));
|
|
329
|
+
// Governance proposer function selectors
|
|
330
|
+
const GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(GovernanceProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
|
|
331
|
+
// Slash factory function selectors
|
|
332
|
+
const CREATE_SLASH_PAYLOAD_SELECTOR = toFunctionSelector(SlashFactoryAbi.find((x)=>x.type === 'function' && x.name === 'createSlashPayload'));
|
|
333
|
+
// Empire slashing proposer function selectors
|
|
334
|
+
const EMPIRE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
|
|
335
|
+
const EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'submitRoundWinner'));
|
|
336
|
+
// Tally slashing proposer function selectors
|
|
337
|
+
const TALLY_VOTE_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'vote'));
|
|
338
|
+
const TALLY_EXECUTE_ROUND_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'executeRound'));
|
|
339
|
+
/**
|
|
340
|
+
* All valid contract calls that the sequencer publisher can make.
|
|
341
|
+
* Builds the list of valid (address, selector) pairs for validation.
|
|
342
|
+
*
|
|
343
|
+
* Alternatively, if we are absolutely sure that no code path from any of these
|
|
344
|
+
* contracts can eventually land on another call to `propose`, we can remove the
|
|
345
|
+
* function selectors.
|
|
346
|
+
*/ function computeValidContractCalls(addresses) {
|
|
347
|
+
const { rollupAddress, governanceProposerAddress, slashFactoryAddress, slashingProposerAddress } = addresses;
|
|
348
|
+
const calls = [];
|
|
349
|
+
// Rollup contract calls (always present)
|
|
350
|
+
calls.push({
|
|
351
|
+
address: rollupAddress.toString().toLowerCase(),
|
|
352
|
+
functionSelector: PROPOSE_SELECTOR,
|
|
353
|
+
functionName: 'propose'
|
|
354
|
+
}, {
|
|
355
|
+
address: rollupAddress.toString().toLowerCase(),
|
|
356
|
+
functionSelector: INVALIDATE_BAD_ATTESTATION_SELECTOR,
|
|
357
|
+
functionName: 'invalidateBadAttestation'
|
|
358
|
+
}, {
|
|
359
|
+
address: rollupAddress.toString().toLowerCase(),
|
|
360
|
+
functionSelector: INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR,
|
|
361
|
+
functionName: 'invalidateInsufficientAttestations'
|
|
362
|
+
});
|
|
363
|
+
// Governance proposer calls (optional)
|
|
364
|
+
if (governanceProposerAddress && !governanceProposerAddress.isZero()) {
|
|
365
|
+
calls.push({
|
|
366
|
+
address: governanceProposerAddress.toString().toLowerCase(),
|
|
367
|
+
functionSelector: GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR,
|
|
368
|
+
functionName: 'signalWithSig'
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Slash factory calls (optional)
|
|
372
|
+
if (slashFactoryAddress && !slashFactoryAddress.isZero()) {
|
|
373
|
+
calls.push({
|
|
374
|
+
address: slashFactoryAddress.toString().toLowerCase(),
|
|
375
|
+
functionSelector: CREATE_SLASH_PAYLOAD_SELECTOR,
|
|
376
|
+
functionName: 'createSlashPayload'
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Slashing proposer calls (optional, can be either Empire or Tally)
|
|
380
|
+
if (slashingProposerAddress && !slashingProposerAddress.isZero()) {
|
|
381
|
+
// Empire calls
|
|
382
|
+
calls.push({
|
|
383
|
+
address: slashingProposerAddress.toString().toLowerCase(),
|
|
384
|
+
functionSelector: EMPIRE_SIGNAL_WITH_SIG_SELECTOR,
|
|
385
|
+
functionName: 'signalWithSig (empire)'
|
|
386
|
+
}, {
|
|
387
|
+
address: slashingProposerAddress.toString().toLowerCase(),
|
|
388
|
+
functionSelector: EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR,
|
|
389
|
+
functionName: 'submitRoundWinner'
|
|
390
|
+
});
|
|
391
|
+
// Tally calls
|
|
392
|
+
calls.push({
|
|
393
|
+
address: slashingProposerAddress.toString().toLowerCase(),
|
|
394
|
+
functionSelector: TALLY_VOTE_SELECTOR,
|
|
395
|
+
functionName: 'vote'
|
|
396
|
+
}, {
|
|
397
|
+
address: slashingProposerAddress.toString().toLowerCase(),
|
|
398
|
+
functionSelector: TALLY_EXECUTE_ROUND_SELECTOR,
|
|
399
|
+
functionName: 'executeRound'
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return calls;
|
|
403
|
+
}
|