Skip to content

feat: check calldata against emitted hashes#20486

Open
mrzeszutko wants to merge 1 commit intomerge-train/spartanfrom
mrzeszutko/check-calldata-against-emitted-hashes
Open

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

Conversation

@mrzeszutko
Copy link
Contributor

Summary

  • Add hash-verified "relaxed mode" to the archiver's multicall3 calldata decoder, allowing it to extract propose calldata from multicalls containing unrecognized calls by verifying candidates against attestationsHash and payloadDigest emitted in CheckpointProposed events
  • Relaxed mode activates only when strict whitelist validation fails AND both expected hashes are available; it filters candidate calls by rollup target address + propose selector, then returns the uniquely hash-verified match
  • Reduces reliance on expensive debug_traceTransaction / trace_transaction RPC fallback for calldata extraction
  • Adds comprehensive unit tests covering hash verification, strict/relaxed multicall3 paths, Spire-wrapped multicalls, edge cases (missing hashes, wrong hashes, multiple candidates), and integration-level getCheckpointFromRollupTx scenarios
  • Updates the L1 calldata retrieval README to document the new verification strategy

Details

Hash verification helper

Added verifyProposeCalldataHashes which decodes propose calldata, computes attestationsHash (keccak of ABI-encoded signatures) and payloadDigest (keccak of ABI-encoded propose args), and returns true only when both computed hashes match the expected values from the event. Returns false on any decode error, missing hash, or mismatch.

Extended tryDecodeMulticall3

The method now accepts optional expectedHashes and rollupAddress parameters:

  1. Strict path (existing): validates all calls against the allowlist; returns immediately on success
  2. Relaxed path (new): when strict fails due to unrecognized calls and both hashes are present, collects candidate propose calls (matching rollup address + selector), verifies each against expected hashes, and returns the uniquely verified candidate
  3. Falls through to undefined if zero or multiple candidates verify, or if hashes are incomplete

Plumbing

expectedHashes and rollupAddress are threaded through getCheckpointFromRollupTx -> getProposeCallData -> tryDecodeMulticall3 and tryDecodeSpireProposer, so relaxed verification works for both direct multicall3 and Spire-wrapped multicall3 transactions.

Backwards compatibility

When hashes are missing (older events before TMNT-461), relaxed mode is skipped entirely and behavior is identical to before. The final decodeAndBuildCheckpoint hash validation remains as a safety net.

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.

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.

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.

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

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