Skip to content

[Contract] Add Cross-Chain Payload Replay Protection via Nonce and Domain Separator - #739

Open
iexwr wants to merge 2 commits into
SoroLabs:mainfrom
iexwr:feature/issue-638-crosschain-replay
Open

[Contract] Add Cross-Chain Payload Replay Protection via Nonce and Domain Separator#739
iexwr wants to merge 2 commits into
SoroLabs:mainfrom
iexwr:feature/issue-638-crosschain-replay

Conversation

@iexwr

@iexwr iexwr commented Jul 28, 2026

Copy link
Copy Markdown

Closes #638


Summary

Adds cross-chain payload replay protection to the CrossChainVerifier contract. The previous implementation accepted a raw leaf hash with no domain separation, allowing the same Merkle proof to be replayed across different source chains, different destination contracts, or multiple times against the same contract.

Problem

The original verify_message took a BytesN<32> leaf parameter with no binding to:

  • Chain identity — no chain ID check, so a proof valid on chain A could be submitted on chain B
  • Destination contract — no verifying contract address, so a proof intended for contract X could be submitted to contract Y
  • Uniqueness — no nonce, so the same message could be verified and replayed indefinitely

Changes

contracts/cross_chain_verifier/src/lib.rs

New types:

  • Payload struct with chain_id: u32, destination_contract: Address, nonce: u64, data: Bytes
  • DataKey::NonceUsed(u64) for replay protection storage

verify_message signature change:

  • leaf: BytesN<32>payload: Payload
  • Internally computes the domain-separated leaf hash: sha256(chain_id || destination_contract.to_xdr() || nonce || data)
  • Checks and marks nonce as used to prevent replay

New helper (outside #[contractimpl] for reference params):

  • compute_payload_hash(env, payload) — the domain-separated hash function

contracts/cross_chain_verifier/src/test.rs

Updated tests:

  • test_verify_message_success — uses Payload struct
  • test_verify_message_no_root — uses Payload struct

New tests:

Test What it verifies
test_verify_message_replay_rejected Same nonce on same payload panics with "Nonce already used"
test_verify_message_different_nonce_allowed Different nonces on same tree root both verify
test_compute_payload_hash_differs_by_chain_id Different chain IDs produce different hashes
test_compute_payload_hash_differs_by_nonce Different nonces produce different hashes
test_compute_payload_hash_differs_by_destination Different destination contracts produce different hashes

Verification

cargo build --package cross-chain-verifier
cargo test --package cross-chain-verifier
  • Build: clean, no warnings
  • Tests: 10/10 passing
  • Format: cargo fmt clean
  • No regressions in any other contract (cross-chain-verifier has no dependents)

Design Notes

  • Domain separator format follows the same pattern as typed_data_auth (EIP-712 style)
  • Nonce tracking uses persistent storage so state survives upgrades
  • Nonces are scoped to the contract instance — each CrossChainVerifier deployment has its own nonce namespace
  • The compute_payload_hash helper is outside #[contractimpl] so it can accept &Payload references without Soroban's FFI restrictions

iexwr and others added 2 commits July 29, 2026 00:36
… verification (SoroLabs#638)

Add cross-chain payload replay protection to CrossChainVerifier.

Problem: verify_message accepted a raw leaf hash with no domain
separation, allowing the same Merkle proof to be replayed across
different chains, to different destination contracts, or multiple times.

Solution:
- New Payload struct with chain_id, destination_contract, nonce, data
- compute_payload_hash() helper computing sha256(chain_id || dest ||
  nonce || data), binding every message to a unique chain, contract,
  and nonce
- verify_message now accepts Payload instead of raw leaf hash,
  computes the domain-separated hash internally
- Nonce tracking via NonceUsed storage key to prevent replay on the
  destination contract
- 10 unit tests, all passing:
  - Existing: initialization, double-init, root update, no-root panic
  - Updated: verify_message success with Payload
  - New: replay rejection, different nonce allowed, hash differs by
    chain_id, hash differs by nonce, hash differs by destination
@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@iexwr Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@iexwr

iexwr commented Jul 30, 2026

Copy link
Copy Markdown
Author

@EDOHWARES Hello

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.

[Contract] Add Cross-Chain Payload Replay Protection via Nonce and Domain Separator

1 participant