feat(ismp): prove outbound delivery on-chain by reading the destination's receipt - #148
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A POST dispatched from Orbinum left no trace here once delivered: the explorer had to ask a
public BSC RPC whether the
PostRequestHandledlog existed — an answer nothing verifies.This turns that question into an on-chain proof.
Adds
confirm_delivery(call index 4) and theDeliveryConfirmedevent. spec 13 → 14,transaction_versionstays at 3.Why not the obvious way
Having the destination reply is not possible. Upstream #840 removed
PostResponsefromthe protocol (
ismp/src/messaging.rs: "the protocol no longer carries PostResponse");IsmpModulehas three callbacks andon_responsetakes only aGetResponse. Any designbuilt on "BSC answers us" is dead on arrival.
What the destination does leave is a receipt.
handlers/request.rs:112writesRequestReceipts[commitment] = relayerbefore invoking the receiving module, and:122-125deletes it again if that module errs. So its presence proves delivered andexecuted — a stronger guarantee than the event itself — and it is storage, which is what a
GET proves.
What it proves, precisely
The receipt we read is Hyperbridge's, not BSC's.
ismp-grandpaonly gives this chain thecoprocessor's ISMP child trie root (
consensus.rs:148), so an EVM chain's state is notprovable here at all. Gargantua's proxy re-dispatches through the same
on_acceptthat writesreceipts, so a receipt there means the coprocessor accepted and forwarded it — one hop
short of execution on the far side, but proven rather than taken on an RPC's word.
confirm_deliverytakes its destination fromCoprocessor::get(), never from the caller, soit cannot be pointed at a chain whose child trie we cannot verify.
Correlation without storage
The confirming GET has its own commitment, unrelated to the POST it proves. The link travels
in
DispatchGet.context, which comes back insideGetResponse.get— so there is noStorageMapto clean up, and no orphaned entry when a GET expires.dispatch_getkeeps its signature (it passesDefault::default()): changing it would havemoved
transaction_versionand invalidated offline-signed extrinsics, for no gain.There is no negative event
A response with an absent value emits nothing. It proves the receipt was not there at that
height, which is indistinguishable from asking too early. Nothing in this pallet can claim a
message failed to arrive.
Audit against the official documentation
Two real defects found and fixed:
valuesarrives sorted by key bytes, not in request order.verify_state_proofreturnsa
BTreeMap(state-machines/substrate/src/lib.rs:246). The code used.first(), whichhappens to be right today only because we ask for one key. It now matches by key, with a
test that plants a decoy sorting ahead of it.
(The official docs claim the opposite — "one entry per key in the original request" — and
are wrong.)
contextwas unbounded.MaxBodyLenbounds bodies andMaxGetKeysbounds keys, butthe context travelled uncapped while
dispatch_get's weight is measured per key, notper context byte. Now bounded by
MaxBodyLen.Also validated:
contextused as the docs prescribe,Nonetreated as proven absence,on_responsenever returningErr(the replay warning), the 8MB/85% block length thesolochain guide requires, and the AURA digest being present.
Verification
Against the release binary and the real Gargantua — the test does not invent a receipt: it
reads one Hyperbridge genuinely wrote, feeds the commitment from its key to
confirm_delivery,and asserts the pallet rebuilds the same key byte for byte.
cargo fmt,clippyand theno_stdbuild are clean; compiles with--features hyperbridge-testnet.After merge
0x92c95a9b…on real testnet, with the root arrivingthrough GRANDPA instead of seeded by sudo.
accept_source(Kusama(4009))via sudo —AcceptedSourcesis empty today, so no inboundPOST can land (
InboundCount: 0). That is an extrinsic, not a release.