orderHash
on one chain be verified on the other. To hold, both sides have to hash
the exact same struct in the exact same way — and they do.
One tuple, two chains
There is a single canonical 15-fieldOrder tuple. Both chains build it
from their own direction-specific OrderParams plus the local context
they have (chain id, contract address) before hashing. The digest is
equal across chains by construction, not by parallel maintenance of
two hashers that happen to match.
| Slot | Field | Type |
|---|---|---|
| 1 | orderChainToken | bytes32 |
| 2 | adChainToken | bytes32 |
| 3 | amount | uint256 (order-chain raw units) |
| 4 | bridger | bytes32 |
| 5 | orderChainId | uint256 |
| 6 | orderPortal | bytes32 |
| 7 | orderRecipient | bytes32 |
| 8 | adChainId | uint256 |
| 9 | adManager | bytes32 |
| 10 | adId | string (hashed to bytes32 via keccak256) |
| 11 | adCreator | bytes32 |
| 12 | adRecipient | bytes32 |
| 13 | salt | uint256 |
| 14 | orderDecimals | uint8 (padded to 32 bytes) |
| 15 | adDecimals | uint8 (padded to 32 bytes) |
Typehash
adId participates in the hash as a string member per EIP-712 — it’s
hashed with keccak256 before being concatenated into the struct hash.
orderDecimals and adDecimals are the two trailing scaling fields
described in Decimal scaling.
Minimal domain, on purpose
The EIP-712 domain is intentionally minimal:chainId and no verifyingContract in the domain. Chain ids and
contract addresses live inside the struct instead (slots 5–6, 8–9),
which means the exact same digest is accepted on either chain without a
domain-separator mismatch. A chain-specific verifyingContract would
split the digest in two and break the cross-chain invariant.
Because chain ids and contracts live inside the struct, they’re still
signed — a replay to the wrong chain or wrong portal fails at
validateOrder, not at signature recovery.
How the two sides build it
- OrderPortal (order chain) takes its
OrderParamsand fills in the localorderChainId+orderPortalfromblock.chainidandaddress(this). The remaining fields — notablyadChainId,adManager,adCreator,adRecipient— come from the signed payload. - AdManager (ad chain) does the mirror image: fills in
adChainIdandadManagerfrom its own local context, trusts the signedorderChainId,orderPortal,bridger,orderRecipientfrom the payload.
OrderHash library (contracts/evm/src/libraries/OrderHash.sol);
on Stellar it’s proofbridge-core::eip712. See
Off-chain signing for the JS/TS recipe
that reproduces the digest bit-for-bit off-chain.
Why this matters for proofs
The zk circuit’s public inputs include the order hash modulo the BN254 prime —[nullifier, orderHash % p, targetRoot, sideFlag]. If the two
chains computed different digests for “the same” order, the public
inputs would diverge and no single proof could satisfy both sides. The
cross-chain digest invariant is what makes a single proof usable on
both chains, which is what makes atomic settlement possible without a
trusted relayer.