Skip to content

[WIP] Polkadot-Kusama Bridge support#1029

Draft
karolk91 wants to merge 7 commits into
AcalaNetwork:masterfrom
karolk91:kk-bridge-support
Draft

[WIP] Polkadot-Kusama Bridge support#1029
karolk91 wants to merge 7 commits into
AcalaNetwork:masterfrom
karolk91:kk-bridge-support

Conversation

@karolk91
Copy link
Copy Markdown
Contributor

A chopsticks-native bridge relayer that delivers pallet_bridge_messages outbound messages between two forked bridge hubs, plus the underlying state_getReadProof JSON-RPC handler it needs.

Two user-facing surfaces:

  • Library: connectBridgeHubs(source, destination, { signer }) mirrors connectUpward / connectHorizontal — subscribe once, react to every new source block forever, terminate via handle.disconnect().
  • CLI: chopsticks bridge -r polkadot -p polkadot-bridge-hub -R kusama -P kusama-bridge-hub spins up both ecosystems and wires the relay in both directions.

Architecture

Two ecosystems (Polkadot + Kusama bridge hubs and their parachains) run as independent chopsticks instances wired together by an in-tool relayer. The relayer is the bridge component — it plays the role substrate-relay plays in production.

  source BHP  ─────────────────────────────►   destination BHK
    │ subscribeNewHeads                            │
    │                                              │
    │ per head:                                    │
    │   diff OutboundLanes vs baseline             │
    │   for each new (lane, nonce-range):          │
    │     state_getReadProof([keys], block.hash) ──┼──► dev_setStorage:
    │     findTrieRoot(proof.nodes)                │      ParasInfo / ImportedParaHeads
    │                                              │      / ImportedParaHashes
    │                                              │   author_submitExtrinsic:
    │                                              │      receive_messages_proof(...)
    │                                              │   dev_newBlock

Key design choices:

  1. Skip pallet_bridge_grandpa + pallet_bridge_parachains finality flow. Production substrate-relay submits
    submit_finality_proof_ex (relay finality) → submit_parachain_heads_ex (BHP head proof against relay state) →
    receive_messages_proof. We write ImportedParaHeads directly via dev_setStorage, so only receive_messages_proof actually runs through pallet logic. Tested the same end-to-end, simpler.

  2. state_getReadProof composes upstream + overlays. Chopsticks keeps no full trie, so the handler fetches a base proof from upstream and re-applies chopsticks-side values via the executor's createProof. The proof's recomputed root reflects local overrides; verifiers should derive the expected state_root from the proof bytes, not from chain_getHeader(at).state_root. For chopsticks-only blocks unknown to upstream the request falls back to upstream head.

  3. Write the proof's own root to ImportedParaHeads, not source.header.state_root. They diverge when (a) chopsticks-only blocks force the upstream-head fallback, or (b) dev_setStorage has touched keys covered by the proof. The connector derives the root via findTrieRoot (the unique node hash not referenced as a 32-byte sub-sequence inside any other node).

  4. Pallet names auto-detected by storage shape. Mirrors how connectUpward hardcodes parachainSystem.upwardMessages (cumulus pallet has a fixed name) — bridge pallets are instantiable, so each runtime names them differently (BridgeKusamaMessages on BHP, BridgePolkadotMessages on BHK). Detection finds any pallet exposing outboundLanes + outboundMessages; errors with clear remediation if zero or multiple matches. Same for pallet_bridge_parachains.

  5. Real polkadot.js signing with the no-callback signAndSend form (resolves on pool submission). Callback form deadlocks against chopsticks's manual dev_newBlock. mockSignatureHost: true (chopsticks default) accepts real sigs normally; the signer must hold a balance on dest via dev_setStorage. Matches what production substrate-relay does — bring your own funded key.

  6. First-sight baseline. Subscription records each lane's current latest_generated_nonce as the baseline on first observation, so fresh forks don't replay historical bridge traffic.

Alternatives explored

Full GRANDPA justification forging — the path #695 explored. Build a synthetic GRANDPA authority set, sign justifications, submit submit_finality_proof_ex → relay tracker advances → submit submit_parachain_heads_ex with proofs against relay state → receive_messages_proof. Works but requires also advancing the source's relay chain via setHead so its paras::Heads storage matches what we proved. Three pallets' worth of plumbing for the same end result. We skip all of it by writing ImportedParaHeads directly.

state_getReadProof with no base nodes — Our composition(createProof(upstream_base, overlays)) produces a coherent proof whose root we derive from the bytes via findTrieRoot.

Library-only API (no CLI subcommand) — would have been less work. Rejected because the existing xcm subcommand sets the precedent: bridges deserve the same one-liner ergonomics. CLI auto-detects bridge hubs by shape so the user only types config paths.

In-process connector via Blockchain instances — two Blockchain references and use chopsticks-internal APIs directly rejected because setupNetworks keys topology off a single relay chain (packages/utils/src/index.ts:194), so a two-relay setup can't live in one process today. Out-of-process model via ApiPromise over WS works regardless of process layout and was the right v1.

References

Testing

  • packages/e2e/src/state.test.ts — 3 new tests for state_getReadProof (proof reflects overrides, rejects child keys, rejects empty list)
  • packages/e2e/src/bridge.test.ts — end-to-end against live BHP + BHK forks: injects two outbound messages across separate source blocks, asserts BridgePolkadotMessages.MessagesReceived fires on BHK each time (proves continuous-subscription model)
  • Manual end-to-end via the polkadot-referenda-tester: Fellowship referendum → AHP InitiateTransfer → BHP MessageAccepted → bridge handle delivers → BHK MessagesReceived + RewardRegistered + XCMP-out → AHK MessageQueue.Processed{success:true} + Whitelist.CallWhitelisted

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found an issue where source.query queries the latest state instead of the state at the specific block hash, which can lead to proof generation failures if the chain has advanced.

Comment thread packages/chopsticks/src/bridge.ts Outdated
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