Skip to content

feat(ismp): rebuild request commitments off-chain and bring event types to spec 13 - #3

Merged
nol4lej merged 1 commit into
mainfrom
feat/ismp-commitments
Sep 10, 2026
Merged

nol4lej merged 1 commit into
mainfrom
feat/ismp-commitments

Conversation

@nol4lej

@nol4lej nol4lej commented Sep 10, 2026

Copy link
Copy Markdown
Member

Rebuild request commitments off-chain, and bring event types to spec 13

0.3.0 modelled the ISMP events before runtime spec 13. The chain now emits twenty fields the SDK did not know about, so a consumer compiles today and reads undefined from data that is in fact arriving.

Added

requestCommitment / encodePostRequest / encodeGetRequest — reproduce the commitment of a request from what ismp_queryRequests returns. This is what joins an event to the request it describes, and what a relayer must do to prove one.

A commitment is keccak256(abi.encode(request)) — Solidity ABI, not SCALE. Request::encode() is an inherent method that shadows the SCALE Encode trait (ismp-2606.1.0/src/router.rs:263), so Rust reads as if it were SCALE while emitting 32-byte-word ABI output. Off-chain the difference is silent: a SCALE encoding hashes fine and yields a value the chain has never seen, surfacing much later as UnknownRequest.

Two details that are easy to get wrong, both taken from the pallet's own output rather than inferred:

  • source and dest travel as their display strings ("SUBSTRATE-orbi", "KUSAMA-4009"). The RPC returns exactly these, so its output is hashable as-is — converting to { type: 'Kusama', value: 4009 } first breaks the hash.
  • The GET field order is not the Rust struct's: source, dest, nonce, from, timeoutTimestamp, keys, height, context.

No new dependency: keccak_256 from @noble/hashes, ABI hand-rolled in ~40 lines.

childTrieProof / ISMP_CHILD_TRIE / commitmentKey / receiptKey — what of Hyperbridge is actually readable is its ISMP child trie, not its global state: for the coprocessor ismp-grandpa records state_root = child_trie_root (consensus.rs:142-150). A GET for a global key such as Ismp::Nonce is unverifiable by construction. Uses ismp_queryChildTrieProof, the RPC Tesseract itself uses, with two encoding traps handled and documented.

Changed

Event types match spec 13. New RequestKind. timeoutTimestamp has three states and conflating any two is a bug: 0n is never-expires (upstream's explicit branch, not 1970). GetResponseReceived.height is the only genuine remote block number this pallet emits. On a GET, RequestDispatched.to is our own module — where the answer routes back to, not a recipient.

relayer on the *Handled events is not necessarily an account: the host takes whatever the submitter signed with, verbatim up to 32 bytes.

And the operational fact behind all of this: the public relayer does not deliver GET responses to Substrate chains (tesseract/messaging/messaging/src/events.rs:314-336), so such a GET goes unanswered unless something of yours carries the proof back. The chain verifies it either way.

Breaking

PostRequest.nonce, PostRequest.timeoutTimestamp, GetRequest.nonce, GetRequest.height and GetRequest.timeoutTimestamp are now bigint — the chain declares them u64 and number loses precision past 2^53. The RPC hands them over as JSON numbers, so widen them (BigInt(raw.nonce)) before hashing.

Verification

pnpm check — 865 tests, typecheck ×2, lint, format, build.

The encoder is pinned against two vectors the runtime itself printed (0xe51e53… POST, 0xc7f995… GET), not a reimplementation: a SCALE encoding of the same request also produces a hash, so "it returns a hash" proves nothing. A third test hashes the first real cross-chain read this chain performed (0x532f2d62…, dispatched at #786579, answered at #786580) back to its on-chain commitment, and a fourth confirms that changing any single field changes the result.

childTrieProof was run against live Gargantua: 1462 raw bytes decode to 7 SCALE nodes, one of which keccak-hashes to that height's ismp.childTrieRoot.

Finally, the compiled bundle was checked end to end against testnet: read the request over RPC, widen the u64s, rebuild — matches.

@nol4lej
nol4lej merged commit 6a229bd into main Sep 10, 2026
3 checks passed
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.

1 participant