Prover Return Decoding Guide
This guide explains how to interpret and decode the returns from the CrossL2ProverV2
contract, using a practical multi-rollup example to demonstrate the complete process.
Overview
The setValueFromSource
function demonstrates how to:
- Take a proof from the Prove API
- Validate cross-chain events using the prover contract
- Decode and process the returned data securely
This process involves several critical steps of data validation and decoding that ensure cross-chain security.
Understanding the Interface
CrossL2Prover validateEvent Function
function validateEvent(bytes calldata proof)
returns (
uint32 chainId, // Source chain identifier
address emittingContract, // Emitting contract address
bytes topics, // Concatenated Event topics
bytes unindexedData // Non-indexed event parameters
)
← Return Values
Four key pieces of validated data from the cross-chain event
🗄️ Data Format
Topics as concatenated bytes, unindexed data as ABI-encoded parameters
Raw Return Data Structure
[
11155420, // uint32: chainId (Optimism Sepolia)
"0x24B1D355f5B254aF86860bBe4214aEDe2DB1314E", // address: emittingContract
"0x...", // bytes: topic1 + topic2 + topic3
"0x..." // bytes: ABI encoded non-indexed parameters
]
Event Structure Reference
Understanding the origin event structure is crucial for proper decoding:
event ValueSet(
// → topic[0] = keccak(ValueSet(address,string,bytes,uint256,bytes32,uint256))
address indexed sender, // In topics[1]
string key, // In unindexedData
bytes value, // In unindexedData
uint256 nonce, // In unindexedData
bytes32 indexed hashedKey, // In topics[2]
uint256 version // In unindexedData
);
Topic Distribution: topics[0]
contains the event signature hash, topics[1-2]
contain indexed parameters, and non-indexed parameters are ABI-encoded in unindexedData
.
Step-by-Step Decoding Process
1. Initial Proof Validation
First, validate the proof and extract the basic return values:
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);
What you get:
sourceChainId
: Origin chain identifiersourceContract
: Contract that emitted the eventtopics
: Concatenated event topics (requires parsing)unindexedData
: ABI-encoded parameters (requires decoding)
2. Topics Array Parsing
The topics
bytes must be parsed into individual bytes32
values:
bytes32[] memory topicsArray = new bytes32[](3);
require(topics.length >= 96, "Invalid topics length"); // 3 * 32 bytes
assembly {
let topicsPtr := add(topics, 32) // Skip length prefix
for { let i := 0 } lt(i, 3) { i := add(i, 1) } {
mstore(
add(add(topicsArray, 32), mul(i, 32)),
mload(add(topicsPtr, mul(i, 32)))
)
}
}
Result:
topicsArray[0]
: Event signature hashtopicsArray[1]
: Indexed sender address (padded to 32 bytes)topicsArray[2]
: Indexed hashedKey
3. Event Signature Verification
Always verify the event signature for security:
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
Critical Security Check: This ensures only ValueSet
events are processed and prevents attacks from similar events with different parameter types.
4. Extract Indexed Parameters
Convert the indexed parameters from the topics:
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];
Conversion Logic:
sender
:bytes32
→uint256
→uint160
→address
hashedKey
: Direct use (alreadybytes32
)
5. Decode Non-Indexed Parameters
Extract the remaining parameters from the ABI-encoded data:
(
, // skip key (we use hashedKey from topics)
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(
unindexedData,
(string, bytes, uint256, uint256)
);
Parameters Retrieved:
key
: Skipped (usinghashedKey
from topics instead)value
: The actual data to storenonce
: For replay protectionversion
: For version control
6. Implement Replay Protection
Prevent the same event from being processed multiple times:
// Create and verify unique proof hash for replay protection
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
Critical Security: Applications should implement replay protection using data from their event directly to prevent duplicate processing of the same cross-chain event.
Common Pitfalls & Solutions
Critical Mistake: The validateEvent
function returns event topics as a single bytes
array, NOT a bytes32[]
array.
❌ Incorrect Approach
( , , bytes memory topics, ) = crossL2Prover.validateEvent(proof);
bytes32 eventSig = bytes32(topics[0]); // ❌ Reads first BYTE, not first TOPIC
✅ Correct Implementation
// Example for an event with 3 topics
require(topics.length == 3 * 32, "Invalid topics length");
// 1. Create a memory array
bytes32[] memory topicsArray = new bytes32[](3);
// 2. Use assembly to parse the topics
assembly {
let topicsPtr := add(topics, 32) // Skip bytes length
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}
// 3. ✅ Use the new array for your logic
bytes32 eventSig = topicsArray[0];
Rule of Thumb: Always parse the topics
bytes variable into a bytes32[]
array before use.
Production Security Checklist
Important: Proof for a given event is not unique and should not be used as a unique identifier for replay protection.
- Source Chain Validation
- Contract Validation
- Event Signature Validation
- Replay Protection
1. Source Chain Validation
Prevent event duplication from unauthorized chains:
mapping(uint32 => bool) public allowedSourceChains;
require(allowedSourceChains[sourceChainId], "Invalid source chain");
Purpose: Safeguards against event duplication from different chains.
2. Source Contract Validation
Ensure events only come from authorized contracts:
mapping(uint32 => mapping(address => bool)) public authorizedContracts;
require(
authorizedContracts[sourceChainId][sourceContract],
"Unauthorized source contract"
);
Purpose: Prevents event duplication from unauthorized contracts on allowed chains.
3. Event Signature Validation
Always verify the complete event signature:
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
// Even slight parameter changes generate different hashes
Critical: Validates both event name and exact parameter types/order.
Example of Different Signatures:
"ValueSet(string,address,bytes,uint256,bytes32,uint256)"
→ Different hash
"ValueSet(address,bytes,string,uint256,bytes32,uint256)"
→ Different hash
4. Replay Protection
Prevent duplicate processing of the same cross-chain event:
mapping(bytes32 => bool) public usedUniqueHashes;
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
Purpose: Applications should implement replay protection using data from their event directly to ensure each cross-chain event is processed only once.
Complete Implementation Example
function setValueFromSource(bytes calldata proof) external {
// 1. Validate the proof
(
uint32 sourceChainId,
address sourceContract,
bytes memory topics,
bytes memory unindexedData
) = polymerProver.validateEvent(proof);
// 2. Security validations
require(allowedSourceChains[sourceChainId], "Invalid source chain");
require(authorizedContracts[sourceChainId][sourceContract], "Unauthorized contract");
// 3. Parse topics
bytes32[] memory topicsArray = new bytes32[](3);
require(topics.length == 96, "Invalid topics length");
assembly {
let topicsPtr := add(topics, 32)
mstore(add(topicsArray, 32), mload(topicsPtr))
mstore(add(topicsArray, 64), mload(add(topicsPtr, 32)))
mstore(add(topicsArray, 96), mload(add(topicsPtr, 64)))
}
// 4. Verify event signature
bytes32 expectedSelector = keccak256("ValueSet(address,string,bytes,uint256,bytes32,uint256)");
require(topicsArray[0] == expectedSelector, "Invalid event signature");
// 5. Extract indexed parameters
address sender = address(uint160(uint256(topicsArray[1])));
bytes32 hashedKey = topicsArray[2];
// 6. Decode non-indexed parameters
(
, // skip key
bytes memory value,
uint256 nonce,
uint256 version
) = abi.decode(unindexedData, (string, bytes, uint256, uint256));
// 7. Implement replay protection
bytes32 uniqueHash = keccak256(
abi.encodePacked(sourceChainId, sourceContract, hashedKey, nonce)
);
require(!usedUniqueHashes[uniqueHash], "hashKey already used");
usedUniqueHashes[uniqueHash] = true;
// 8. Process the validated data
_processValidatedData(sender, hashedKey, value, nonce, version);
}
Security First: This example includes all critical security validations for production use, ensuring robust cross-chain event processing.