Skip to main content
Anything that signs on behalf of a bridger, ad creator, or the relayer has to produce bytes that match what the on-chain verifier expects — to the bit. This page is the integrator-facing recipe for two things:
  1. Reproducing the Order EIP-712 digest (used by OrderPortal.validateOrder, AdManager.validateOrder, and as a public input to the zk circuit).
  2. Building pre-auth request hashes for createOrder, unlockOrder, createAd, lockForOrder (used by the relayer to authorise calls off-chain before signing them with an admin key).
The canonical reference implementations live in the monorepo:
  • JS / TSscripts/cross-chain-e2e/lib/signing.ts and scripts/cross-chain-e2e/lib/proof.ts.
  • Fixtures / test vectorscontracts/stellar/tests/fixtures/generate_fixtures.ts.
If you’re building a custom signer or server, mirror those files — the Solidity (contracts/evm/src/libraries/*.sol) and Rust (contracts/stellar/proofbridge-core) sources are authoritative, and the two TS files port them slot-for-slot.

Reproducing the Order digest in TS

The on-chain digest is EIP-712 over a 15-field tuple, with a deliberately minimal domain (see Order hashing for the rationale). The byte layout of the struct hash is:
Note the two decimals fields at the end are uint8 values padded to 32 bytes — they’re still word-aligned in the struct hash, just with 24 leading zero bytes. The full digest is:
with domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256("ProofBridge"), keccak256("1"))). No chainId or verifyingContract in the domain — see Order hashing for why.

Sanity check against a known vector

contracts/stellar/tests/fixtures/generate_fixtures.ts produces a fixture whose digest is verified on both the EVM and Stellar test suites. If your TS implementation lands on the same orderHash for the same input, you’re wire-compatible with both chains.

Pre-auth request hashes

The relayer doesn’t hand callers arbitrary signatures. Instead, each on-chain operation has a pre-auth request — an off-chain payload the relayer’s admin key signs to authorise this specific call by this specific user before this specific deadline. On-chain, the portals recover the signer from the request hash and compare it to the registered admin address. The hash is uniform across all operations:
Every operation uses the same outer shell — the only part that changes is action (a fixed string) and params (the packed operation-specific fields).

Action strings and params

Canonical TS implementations for each live in scripts/cross-chain-e2e/lib/signing.ts under hashCreateOrderRequest, hashUnlockOrderRequest, hashCreateAdRequest, hashLockForOrderRequest.

Endianness is load-bearing

Two details are easy to get wrong on the JS side:
  • time_to_expire (u64) is big-endian, 8 bytes.
  • chain_id (u128) is big-endian, 16 bytes — not a uint256. This is so the same hash works on EVM (chain ids fit in less than 16 bytes) and Stellar (which uses a locally-generated u128 as its chain id).
The helpers in signing.ts handle both. If you’re writing your own and the contract rejects your signature, it’s almost always one of these two — dump your packed bytes, compare slot-by-slot against the reference.

Running the reference off-chain signer

The e2e harness in scripts/cross-chain-e2e is the simplest way to see the full flow end-to-end: it produces signed orders + pre-auth requests, submits them to a local docker stack, and asserts on-chain state converges. Follow Run locally to spin it up — the same functions it calls are what you’d import into a server of your own.

Public inputs to the zk circuit

Once you have an order digest, the public-input vector the Noir circuit expects is:
where p is the BN254 scalar field prime and sideFlag is 0 on the order chain and 1 on the ad chain. The % p reduction is essential — without it the value won’t fit as a field element and the on-chain verifier will reject the proof even if everything else is right. See scripts/cross-chain-e2e/lib/proof.ts for the exact reduction used by the harness.