feat: Add optional payload hash to foreign transaction requests - #4032
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses audit issue #3997 by adding an optional expected_payload_hash to foreign transaction verification requests, allowing the contract (and nodes) to reject replayed/stale responses whose signed payload_hash does not match the request’s expectation. It updates the contract-interface DTOs, node request plumbing + signing behavior, SDK request builder behavior, and adds/updates documentation and tests (unit, sandbox, and e2e) to validate the new binding semantics.
Changes:
- Add
expected_payload_hash: Option<Hash256>toVerifyForeignTransactionRequestArgs/VerifyForeignTransactionRequest, with JSON omission whenNone. - Enforce request/response binding:
- Contract:
respond_verify_foreign_txrejects mismatching payload hashes. - Node: refuses to sign when computed hash differs from request expectation.
- Contract:
- Update SDK builder to compute and populate
expected_payload_hash, and extend tests/docs/e2e to cover hash-bound requests.
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/foreign-chain-transactions.md | Documents the new optional expected_payload_hash binding and updated failure behavior. |
| crates/node/src/types.rs | Threads expected_payload_hash through the node’s internal VerifyForeignTxRequest. |
| crates/node/src/tests.rs | Updates node test helper to set expected_payload_hash explicitly. |
| crates/node/src/providers/verify_foreign_tx/sign.rs | Rejects signing when computed payload hash doesn’t match the request’s expected hash; adds unit tests. |
| crates/node/src/indexer/types.rs | Passes expected_payload_hash through indexer respond-args construction. |
| crates/node/src/indexer/handler.rs | Plumbs expected_payload_hash out of on-chain args into node request handling. |
| crates/near-mpc-sdk/tests/starknet.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/polygon.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/hyper_evm.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/bnb.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/bitcoin.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/base.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/arbitrum.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/tests/abstract_chain.rs | Adjusts SDK tests for new fallible builder return type (build().unwrap()). |
| crates/near-mpc-sdk/src/foreign_chain/starknet.rs | Updates expected request args to include expected_payload_hash computed from expected extracted values. |
| crates/near-mpc-sdk/src/foreign_chain/polygon.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain/hyper_evm.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain/evm.rs | Updates expected request args to include expected_payload_hash computed from expected extracted values. |
| crates/near-mpc-sdk/src/foreign_chain/bnb.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain/bitcoin.rs | Updates expected request args to include expected_payload_hash computed from expected extracted values. |
| crates/near-mpc-sdk/src/foreign_chain/base.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain/arbitrum.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain/abstract_chain.rs | Updates chain-specific tests to unwrap the now-fallible builder. |
| crates/near-mpc-sdk/src/foreign_chain.rs | Refactors payload-hash computation into a helper, makes builder return io::Result, and sets expected_payload_hash in request args. |
| crates/near-mpc-contract-interface/src/types/foreign_chain.rs | Adds expected_payload_hash fields to DTOs and adds a ForeignTxSignPayload::new(...) constructor. |
| crates/near-mpc-contract-interface/src/snapshots/near_mpc_contract_interface__client__tests__mpc_contract_handle__should_match_the_wire_format_catalog.snap | Updates wire-format snapshot to include the new optional field when present. |
| crates/near-mpc-contract-interface/src/client.rs | Extends client wire-format catalog test to cover Some/None serialization behavior for expected_payload_hash. |
| crates/e2e-tests/tests/foreign_chain_tx_validation.rs | Adds an e2e scenario using the SDK builder to submit a hash-bound request and asserts response hash matches expectation. |
| crates/e2e-tests/Cargo.toml | Adds near-mpc-sdk dependency for e2e tests that build requests via the SDK. |
| crates/contract/tests/snapshots/abi__abi_has_not_changed.snap | Updates contract ABI snapshot with the new optional field in request DTOs. |
| crates/contract/tests/sandbox/foreign_chain_request.rs | Adds sandbox tests asserting the contract rejects mismatched payload hashes and accepts matching ones when bound. |
| crates/contract/src/v3_13_0_state.rs | Migration logic updated to use a new storage key for pending verify-foreign-tx requests and documents abandoned in-flight entries. |
| crates/contract/src/storage_keys.rs | Deprecates V2 storage key and adds PendingVerifyForeignTxRequestsV3. |
| crates/contract/src/lib.rs | Enforces expected payload hash check in respond_verify_foreign_tx; updates init storage key. |
| crates/contract/src/errors.rs | Adds RespondError::UnexpectedPayloadHash. |
| crates/contract/src/dto_mapping.rs | Maps expected_payload_hash from args into on-chain request struct. |
| Cargo.lock | Captures dependency graph changes (notably adding near-mpc-sdk to e2e-tests). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Pull request overviewAdds an optional Changes:
Reviewed changesPer-file summary
The security property holds: the expectation is part of the borsh FindingsNon-blocking (nits, follow-ups, suggestions):
✅ Approved |
b31d761 to
a2544a3
Compare
This sounds like a potential good point, but orthogonal to this PR and I don't have the capacity to distinguish this statement from slop/hallucinations atm so I will leave this for now. |
Looked deeper into this and created a follow-up as it may be a good thing to look into #4079 |
014b6a7 to
1d14865
Compare
cd82487 to
cf5fe59
Compare
build() and verify_signature() each constructed the payload from the verifier's fields; if they drifted, every SDK-built request would carry a hash the network never signs. The shared helper also pairs the payload variant with DEFAULT_PAYLOAD_VERSION via an exhaustive match in the interface crate, so adding a payload version breaks compilation there instead of silently unpairing them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
respond_verify_foreign_tx has several unrelated early-return errors, so a bare is_err() could stay green while the payload-hash check is unreachable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The omitted form is the compatibility-critical one: contracts predating the field must never see it. Keep it pinned alongside the populated form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Builds the request through the SDK builder so the SDK-derived payload hash must match the payload the nodes derive independently; the contract rejects the response otherwise. Catches SDK/node payload encoding divergence at same-repo versions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also reword "yields time out" to "yielded promises time out" in the migration comment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cf5fe59 to
7af44d7
Compare
closes #3997
Supersedes #4008 which was closed during a hack