Skip to content

Commit 5eb4e9d

Browse files
committed
synth: read a block's metadata from its parent, not itself
1 parent 369c701 commit 5eb4e9d

1 file changed

Lines changed: 107 additions & 5 deletions

File tree

node/src/synthetic_logs/storage_override.rs

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use sc_client_api::{backend::Backend, BlockBackend, StorageProvider};
3636
use sp_api::{Metadata as MetadataApi, ProvideRuntimeApi};
3737
use sp_blockchain::HeaderBackend;
3838
use sp_core::{hashing::twox_128, H160, H256, U256};
39-
use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Hash as HashT};
39+
use sp_runtime::traits::{BlakeTwo256, Block as BlockT, Hash as HashT, Header as HeaderT};
4040
use sp_storage::StorageKey;
4141

4242
type Hash = <Block as BlockT>::Hash;
@@ -60,6 +60,40 @@ fn names(missing: &[Missing]) -> String {
6060
missing.iter().map(|m| m.name.as_str()).collect::<Vec<_>>().join(", ")
6161
}
6262

63+
/// Which block's state to read the metadata out of, to describe the runtime that *executed*
64+
/// `at`.
65+
///
66+
/// The parent's, not `at`'s. A runtime api call at `at` runs whatever `:code` sits in `at`'s
67+
/// post-state, and on the block where an upgrade is applied cumulus has already written the
68+
/// NEW code there while the block itself was executed by the OLD one. Reading metadata at
69+
/// `at` would then cache the new runtime's layout under the old runtime's spec version and
70+
/// mis-decode every later old-spec block. The parent's post-state holds the code that
71+
/// actually ran, in every case:
72+
///
73+
/// ```text
74+
/// block N-1 ─┬─ :code = old
75+
/// block N ├─ executed by old; on_initialize writes :code = new; LastRuntimeUpgrade = old
76+
/// block N+1 └─ executed by new; LastRuntimeUpgrade = new
77+
///
78+
/// layout for N → spec old (from N) + metadata from N-1 (old code) ✓
79+
/// layout for N+1 → spec new (from N+1) + metadata from N (new code) ✓
80+
/// ```
81+
///
82+
/// Falls back to `at` at genesis, or when the parent is unknown — no worse than before, and
83+
/// the odds of the oldest block this node retains being an upgrade block are negligible.
84+
fn metadata_source<C>(client: &C, at: Hash) -> Hash
85+
where
86+
C: HeaderBackend<Block>,
87+
{
88+
client
89+
.header(at)
90+
.ok()
91+
.flatten()
92+
.map(|header| *header.parent_hash())
93+
.filter(|parent| *parent != Hash::default())
94+
.unwrap_or(at)
95+
}
96+
6397
/// Log the first occurrence, then back off by powers of two, so persistent loss stays
6498
/// visible without flooding one line per block.
6599
fn should_report(counter: &AtomicUsize) -> usize {
@@ -132,14 +166,16 @@ where
132166
if let Some(known) = layouts.get(&spec) {
133167
return known.clone();
134168
}
135-
let layout = self.build_layout(at, spec);
169+
let layout = self.build_layout(metadata_source(self.client.as_ref(), at), spec);
136170
layouts.insert(spec, layout.clone());
137171
layout
138172
}
139173

140-
/// The spec version running at `at`, out of state rather than the runtime api: no wasm
141-
/// call, and `frame_executive` keeps `System::LastRuntimeUpgrade` equal to the current
142-
/// runtime's version at every block after an upgrade. Only the leading `Compact<u32>` of
174+
/// The spec version of the runtime that *executed* `at`, out of state rather than the
175+
/// runtime api: no wasm call, and `frame_executive` only bumps
176+
/// `System::LastRuntimeUpgrade` when a new runtime runs for the first time, so the value
177+
/// in a block's post-state names the runtime that produced that block's events — which is
178+
/// exactly the layout needed to decode them. Only the leading `Compact<u32>` of
143179
/// `LastRuntimeUpgradeInfo` is read, so the `spec_name` type moving between sdks cannot
144180
/// break it. A missing entry just shares one cache slot.
145181
fn spec_version(&self, at: Hash) -> u32 {
@@ -449,3 +485,69 @@ where
449485
self.inner.is_eip1559(at)
450486
}
451487
}
488+
489+
#[cfg(test)]
490+
mod tests {
491+
use super::*;
492+
use sp_runtime::generic::Header as GenericHeader;
493+
use sp_runtime::traits::BlakeTwo256 as Hashing;
494+
495+
type Header = GenericHeader<u32, Hashing>;
496+
497+
/// Just enough of a header backend to answer "what is this block's parent".
498+
struct Headers(Vec<(Hash, Hash)>);
499+
500+
impl HeaderBackend<Block> for Headers {
501+
fn header(&self, hash: Hash) -> sp_blockchain::Result<Option<Header>> {
502+
Ok(self.0.iter().find(|(h, _)| *h == hash).map(|(_, parent)| Header {
503+
parent_hash: *parent,
504+
number: 1,
505+
state_root: Default::default(),
506+
extrinsics_root: Default::default(),
507+
digest: Default::default(),
508+
}))
509+
}
510+
fn info(&self) -> sp_blockchain::Info<Block> {
511+
unimplemented!("not read by metadata_source")
512+
}
513+
fn status(&self, _: Hash) -> sp_blockchain::Result<sp_blockchain::BlockStatus> {
514+
unimplemented!("not read by metadata_source")
515+
}
516+
fn number(&self, _: Hash) -> sp_blockchain::Result<Option<u32>> {
517+
unimplemented!("not read by metadata_source")
518+
}
519+
fn hash(&self, _: u32) -> sp_blockchain::Result<Option<Hash>> {
520+
unimplemented!("not read by metadata_source")
521+
}
522+
}
523+
524+
/// The upgrade-block regression: metadata has to come from the parent's state, because a
525+
/// runtime api call at the upgrade block itself would run the NEW `:code` cumulus has
526+
/// already written there, and cache the new layout under the old runtime's spec version.
527+
#[test]
528+
fn metadata_comes_from_the_parent_state() {
529+
let (parent, upgrade) = (Hash::repeat_byte(1), Hash::repeat_byte(2));
530+
let headers = Headers(vec![(upgrade, parent)]);
531+
assert_eq!(
532+
metadata_source(&headers, upgrade),
533+
parent,
534+
"a block's events were produced by the runtime in its PARENT's post-state"
535+
);
536+
}
537+
538+
/// Genesis has no parent, and an unknown block cannot offer one: fall back to the block
539+
/// itself rather than reading state at the zero hash.
540+
#[test]
541+
fn genesis_and_unknown_blocks_fall_back_to_themselves() {
542+
let genesis = Hash::repeat_byte(3);
543+
let headers = Headers(vec![(genesis, Hash::default())]);
544+
assert_eq!(metadata_source(&headers, genesis), genesis, "genesis has no parent");
545+
546+
let orphan = Hash::repeat_byte(9);
547+
assert_eq!(
548+
metadata_source(&Headers(Vec::new()), orphan),
549+
orphan,
550+
"an unknown header must not send us to the zero hash"
551+
);
552+
}
553+
}

0 commit comments

Comments
 (0)