@@ -36,7 +36,7 @@ use sc_client_api::{backend::Backend, BlockBackend, StorageProvider};
3636use sp_api:: { Metadata as MetadataApi , ProvideRuntimeApi } ;
3737use sp_blockchain:: HeaderBackend ;
3838use 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 } ;
4040use sp_storage:: StorageKey ;
4141
4242type 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.
6599fn 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