fix: read events when the node's sdk drifts from the runtime's - #1503
Merged
Conversation
Weight Diff Report
35 extrinsic(s) changed across 2 pallet(s). New: 4. Removed: 2. pallet_omnipool_liquidity_mining
pallet_stableswap
New extrinsics (4)
Removed extrinsics (2)
Threshold: ±10%. Base |
|
Crate versions that have been updated:
Runtime version has been increased. |
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.
Three bugs in the node-side synthetic-logs layer, plus an envelope rework so synth txs are legible to ordinary EVM tooling.
1. Node/runtime sdk skew silently zeroed synth logs (live on mainnet)
v49.2.0is a node-only release onpolkadot-stable2603while the runtime stays onstable2506. Upstream polkadot-sdk#7250 insertedMintedCredit(11) andBurnedDebt(13) into the middle ofpallet_balances::Event, shiftingBurned11→12,Issued15→17,Rescinded16→18,Locked17→19,Frozen19→21.Synth logs decode
System::Eventswith the node's compiledRuntimeEvent, so the chain'sIssued{amount}(16 B) is read asRestored{who,amount}(48 B) → the wholeVec<EventRecord>decode fails →Decode::decode(..).ok()turns that intoVec::new()and the block looks event-free. No log, no metric, no RPC error.Measured on mainnet:
rpc.hydradx.cloudandhydration-rpc.n.dwellir.comreturn 0 logs for every block paying fees in native HDX (~18% of blocks), including the wormhole core-bridgeLogMessagePublishedraised by substrate-dispatchedEVM.call— so guardians never observed NTT DAI seq 2–5.Fix —
compat_events::read_events, in descending fidelity:Native→ typed decode, unchanged fast path, free when node and runtime agreeCompat→ explicit per-sdkpallet_balances::Eventlayout, other pallets via the node's own types → exactPartial→ resync + a whole-blobEVM.Logsweep (see §3)2. Tx-hash domain keyed on the parent hash → collisions on every fork
block_domainfoldedparent_hash + block_number. Sibling blocks at the same height share a parent, so every field feedingkeccak(RLP(tx))was identical for both — two blocks minting the same synth tx hash, which frontier indexes by hash. On a chain whose fork rate warranted an emergency release, that is live. Now keyed on the block's own hash, distinct by construction.3.
Partiallost runs of records, and did so silentlyTwo compounding defects, both found by replaying block 13384676 (the DAI transfer that published wormhole sequence 2) on a skewed node:
LogMessagePublished. The record itself was intact at byte offset 1710 (phase=ApplyExtrinsic(3),pallet=0x5a,variant=0x00); a mis-decoded neighbour ate it.CompatandPartialshared oneOnce, so once a recoverable skew reported itself, all subsequent real data loss went unreported.There is also a second sdk skew beyond balances: that block carries
ConvictionVoting.VotedandScheduler.{Scheduled,Canceled}, whose layouts also moved between stable2506 and stable2603. Per-pallet compat layouts are therefore whack-a-mole and cannot be a guarantee.Fix — a whole-blob sweep for
EVM.Logrecords, deduped and merged into whatever the sequential walk produced. It works because anEVM.Logrecord's bytes depend on nothing that skews:phase ‖ pallet ‖ variant ‖ address ‖ topics ‖ data ‖ record_topics. Guards: pallet byte must be this runtime's EVM index, variant must beLog, ≤4 log topics, empty record-topic vec.Partialnow reports on its own counter with power-of-two backoff and carries arecoveredcount.This guarantees the wormhole class specifically. Other records (e.g. ERC-20
Transfer) can still be incomplete on a doubly-skewed node — only metadata-driven decoding, or a node whose runtime matches the chain, closes that gap.4. Envelope rework
from0xdeadbeef…beef0x73796e74680000000000000000000073796e7468— asciisynth..........synthtoTransactionFeePaid), sentinel when there is nonevalue0— nothing should see a phantom native transfernoncebucket_nonce, up to2^64-4(2×i64::MAX)(block << 16) | group— monotonic, unique per (block, group), int64-safeinputmarker ‖ parent_hash ‖ block_numberhydrationSynthV1(bytes32,bytes32,uint64), selector0xf1f8393ainputis now an ordinary EVM call, soethers/viem/castdecode it with the signature alone and explorers with a 4byte lookup render it as a named call; versioning rides on the selector, so a layout change makes stale decoders fail loudly instead of misreading bytes. Verified live withcast decode-calldata, bothbytes32values cross-checked against the substrate side.Rationale for the rest: the old hook nonces overflow the
bigintcolumn indexers keep nonces in (and ~92% of synth txs are hook buckets); a repeating(from, nonce)breaks it as a key and trips replacement-detection;fromstays the marker so no unsigned tx is attributed to a user and no real account's nonce sequence is entangled;extrinsicHashininputmakes substrate↔EVM correlation a field read. HashingOpaqueExtrinsicneeds noRuntimeCalldecode, so none of it is exposed to §1.Version bump
hydradx-runtime429→430 +spec_version429→430, because the fix touchesruntime/hydradx/src/evm/and version-check requires modified crates bumped with runtime major ==spec_version. Bookkeeping only — the synth path is native code in the node binary (the runtime API was removed in9c4da722c), so this still ships as a node-only release. Demonstrated: the fixed node reports compiled runtime 429 while executing chain spec 433 and serves the fixed logs.Verification
Tests — 19
synthetic_logs, 11event_logs, 6 node = 36. Notablynode_balances_layout_is_known(fails on the next mid-enum event insertion),synth_selector_matches_signature, andwormhole_event_survives_undecodable_neighbours— which replays real mainnet events from block 13384676 and asserts sequence 2 comes out.On a real node — built this branch, ran it against the pruned mainnet snapshot, synced to head:
rpc.hydradx.cloud: blocks recovered with 2–113 logs each, never fewereth_getTransactionByHash+ receipt and appear inblock.transactionsNot a substitute for putting eth-rpc nodes on a release whose runtime matches the chain (
v50.0.0today).