Skip to content

feat: check calldata against emitted hashes#20486

Open
mrzeszutko wants to merge 3 commits intomerge-train/spartanfrom
mrzeszutko/check-calldata-against-emitted-hashes
Open

feat: check calldata against emitted hashes#20486
mrzeszutko wants to merge 3 commits intomerge-train/spartanfrom
mrzeszutko/check-calldata-against-emitted-hashes

Conversation

@mrzeszutko
Copy link
Contributor

@mrzeszutko mrzeszutko commented Feb 13, 2026

Summary

  • Replace the allowlist-based "strict" multicall3 validation with hash-only matching: the archiver now finds propose calls by rollup address + selector and verifies them against attestationsHash and payloadDigest from CheckpointProposed events
  • Make expected hashes required throughout — drop backwards compatibility for older events without hashes
  • Simplify CalldataRetriever constructor from a contract addresses object to just rollupAddress
  • Remove the allowlist infrastructure: ValidContractCall, computeValidContractCalls, all non-propose selector constants, and unused ABI imports (EmpireSlashingProposerAbi, GovernanceProposerAbi, SlashFactoryAbi, TallySlashingProposerAbi)
  • Update Spire Proposer to return all wrapped calls (not just exactly one), enabling hash matching across multi-call Spire transactions
  • Fix the CLI debug tool (retrieve-calldata.ts) to extract hashes from CheckpointProposed event logs and pass them to the retriever
  • Remove l1Addresses constructor parameter from ArchiverL1Synchronizer and contractAddresses from data retrieval functions

Details

Hash-only matching in tryDecodeMulticall3

Previously, the method first tried "strict" validation (all calls must be on an allowlist of known addresses and selectors), then fell back to "relaxed" hash matching. Now there's a single path: find all calls matching rollup address + propose selector, verify each candidate against expected hashes, return the uniquely verified one. If multiple candidates verify (identical data), the first is returned with a warning.

Required expected hashes

attestationsHash and payloadDigest are now required (not optional) in:

  • CheckpointProposedArgs type in rollup.ts
  • All CalldataRetriever methods (getCheckpointFromRollupTx, getCheckpointFromTx, tryDecodeMulticall3, tryDecodeDirectPropose, tryDecodeSpireProposer, tryDecodeAndVerifyPropose)

Runtime guards in getCheckpointProposedEvents throw if either field is missing from the event log.

Simplified constructor

CalldataRetriever now takes just rollupAddress: EthAddress instead of { rollupAddress, governanceProposerAddress, slashingProposerAddress, slashFactoryAddress? }. This eliminates the need for l1Addresses in ArchiverL1Synchronizer and contractAddresses in data retrieval functions.

Spire Proposer multi-call support

getCallFromSpireProposer renamed to getCallsFromSpireProposer, now returns all wrapped calls as an array. tryDecodeSpireProposer iterates each call and tries it through tryDecodeMulticall3 or direct propose + hash verification, returning the first verified match.

CLI debug tool

retrieve-calldata.ts now uses decodeEventLog from viem to extract attestationsHash and payloadDigest from the CheckpointProposed event log, passing real hashes instead of an empty object.

Test plan

  • All 55 calldata retriever unit tests pass (hash matching, multicall3, direct propose, Spire Proposer, trace fallback, integration)
  • All 17 spire proposer unit tests pass (single/multi call, validation failures, array return type)
  • Build, format, and lint pass cleanly

Fixes A-408

@mrzeszutko mrzeszutko changed the title check calldata against emitted hashes feat: check calldata against emitted hashes Feb 13, 2026
expectedHashes?: { attestationsHash?: Hex; payloadDigest?: Hex },
): Promise<Hex | undefined> {
// Try to decode as Spire Proposer multicall (extracts the wrapped call)
const spireWrappedCall = await getCallFromSpireProposer(tx, this.publicClient, this.logger);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to extend the multicall strict/relaxed logic to the spire proposer as well. See Spire Proposer multicall must contain exactly one call in archiver/src/l1/spire_proposer.ts. If we can now identify which one is the correct call, we should do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed - I missed this before

Comment on lines 275 to 278
if (proposeCalls.length > 1) {
this.logger.warn(`Multiple propose calls found in multicall3 (${proposeCalls.length})`, { txHash });
return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, this means that if there are two propose calls on the multicall, and all other calls are whitelisted, we'll still fail rather than fall back to comparing by expected hashes. Granted, we should not have more than one propose call per multicall, but still - not failing here means we don't need to fall back to debug traceTx.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to return the first call if there is more than one with the same hashes, if there is more than 1 it is now just logged

Comment on lines 185 to 188
* Attempts to decode transaction input as multicall3 and extract propose calldata.
* Returns undefined if validation fails.
* Tries strict validation first (all calls must be on the allowlist). If strict fails due to
* unrecognized calls and both expected hashes are available, falls back to relaxed mode which
* filters candidate propose calls by target address + selector and verifies them against hashes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just drop the strict/relaxed differentiation, fetch all propose calls we find in the multicall (ignoring the ones that don't match, and removing altogether the "valid functions" check), and then filter by the ones that match the expected hashes. There's no point in keeping backwards compatibility for L1 rollup contracts that don't emit the expected hashes in the event, since we're developing on next directly without having to backport.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed backwards compatiblity and refactored the code to remove doubled hash validation

Comment on lines 493 to 495
if (!attestationsMatch) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some warn-level logging here, since this should not happen

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants