Skip to content

feat(ismp): dispatch_get, spec-13 event fields, per-target slot duration, honest GET delivery caveat - #147

Merged
nol4lej merged 2 commits into
mainfrom
feat/ismp-spec13-event-fields
Sep 10, 2026
Merged

feat(ismp): dispatch_get, spec-13 event fields, per-target slot duration, honest GET delivery caveat#147
nol4lej merged 2 commits into
mainfrom
feat/ismp-spec13-event-fields

Conversation

@nol4lej

@nol4lej nol4lej commented Sep 10, 2026

Copy link
Copy Markdown
Member

Spec 13 — dispatch_get, spec-13 event fields, per-target Hyperbridge slot duration

spec_version 12 → 13, transaction_version stays at 3 (events are metadata; adding call_index(3) leaves indices 0–2 and their encodings untouched, so offline-signed extrinsics still decode). No migration, no storage change.

What changes

Every ismpMessaging event now carries what the callback already had in hand. Spec 12 made inbound events attributable with commitment; this adds the rest so no further runtime upgrade is needed for cross-chain observability.

event new fields
RequestDispatched nonce, timeout_timestamp, body_len, kind
MessageReceived nonce, timeout_timestamp
MessageRejected body_len, nonce, timeout_timestamp
GetResponseReceived dest, height, nonce, timeout_timestamp
RequestTimedOut kind, nonce, timeout_timestamp, body_len

New RequestKind { Post, Get } (1 byte), shared by RequestDispatched and RequestTimedOut. timeout_timestamp = 0 means never expires — upstream's own branch, not now + 0.

New extrinsic dispatch_get(dest, keys, height, timeout) at call_index(3), root-only like dispatch_post. It exists because a POST can be refused and a GET cannot: pallet-ismp-demo on Hyperbridge rejects any Substrate(_) source and the relayer dry-runs before submitting, so our POSTs were dropped silently. A GET has no receiving module — this chain verifies the read against a commitment of dest it already holds.

Guards fail an unanswerable GET at dispatch instead of at expiry: empty keys (NoKeysRequested), more than MaxGetKeys = 16 (TooManyKeys — each key is a membership proof a remote chain must produce), height == 0 (InvalidGetHeight — the response handler compares heights for equality), dest == self (DestinationIsSelf). Weight is not benchmarked: reuses dispatch_post's base with a generous per-key term (over-charges, never under-charges).

HYPERBRIDGE_SLOT_DURATION_MS now follows the build feature: 12000 on Polkadot, 6000 on Paseo (was 6000 for both). ismp-grandpa dates every Hyperbridge header as aura_slot × slot_duration; measured live, the old value would have dated every mainnet state commitment ~28 years early. Testnet builds are unchanged.

Two caveats, stated in the code rather than discovered later

  • Who answers a GET. Tesseract delivers GET responses only to EVM sources (tesseract/messaging/messaging/src/events.rs:314-336). On this chain the answer is carried by scripts/hyperbridge/relay-get-response.mjs, a legitimate permissionless ISMP relayer: it ferries the GetRequest plus Hyperbridge's own state proof into Ismp.handle_unsigned; the chain verifies it against the GRANDPA-tracked commitment. The relayer adds no trust.
  • What of Hyperbridge is readable. For the coprocessor, ismp-grandpa stores state_root = child_trie_root (consensus.rs:142-150), verified live against Gargantua. A GET to Hyperbridge can only prove keys inside :child_storage:default:ISMPv2, with a Keccak proof from ismp_queryChildTrieProof. A GET for a global key can never verify — for any relayer.

Verification

  • cargo test -p pallet-ismp-messaging40 (incl. dispatched_fields_rebuild_the_committed_request, a_get_response_closes_out_the_get_it_answers, commitments_hash_the_abi_encoding_not_scale with vectors shared with the JS harness, a_get_response_is_accepted_without_any_accepted_source)
  • cargo test -p orbinum-runtime45 on both targets (slot_duration_follows_the_coprocessor pins the docs' table per feature)
  • scripts/hyperbridge/run-tests.sh all against a dev node — e2e 61, security 43 (incl. all dispatch_get guards and no nonce consumed on rejection), roundtrip 24 (two dev nodes; on_response executed on-chain through handle_unsigned with a real state proof, not in the mock)
  • relay-get-response.mjs --bootstrap-local28/28: a local spec-13 node seeded with Gargantua's live child-trie root answered a GET with a real proof fetched from Gargantua; GetResponseReceived with the requested height, found = 1
  • test-interop-preflight.mjs against testnet — 18/19, the only failure is spec ≥ 13 (this release); consensus is fresh both ways (Gargantua ≤ 8 blocks behind us, we ≤ 11 behind it), our commitment equals Gargantua's real ismp.childTrieRoot

Findings pinned by tests (so they cannot regress silently)

  • A commitment is keccak256(abi.encode(request)) — Solidity ABI, not SCALE. Request::encode() is an inherent method that shadows the Encode trait; source/dest travel as display strings ("SUBSTRATE-orbi"); the GET field order is source, dest, nonce, from, timeoutTimestamp, keys, height, context.
  • Whitelisting in ismp-grandpa gates state proofs too, not only consensus proofs (state_machine() consults SupportedStateMachines); validate_unsigned collapses every failure to BadProof, which prints as "Transaction has a bad signature". Run the node with -l ismp=debug for the real cause.

@nol4lej
nol4lej merged commit 53640f3 into main Sep 10, 2026
6 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