From 8669f293dfc3ebcd5863d9f3654f2cfd1ec72238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20K=C3=B6cher?= Date: Wed, 18 Feb 2026 22:41:18 +0100 Subject: [PATCH 1/4] polkadot-runtime-api-cache: Only cache validation code that exists Otherwise there is the possibility that nodes cache `None` and some later `force_set_code` enacts the code. Then the nodes that have cached `None` do not know that the validation code now actually exists on chain. --- polkadot/node/core/runtime-api/src/cache.rs | 40 ++++++++++++++++++--- polkadot/node/core/runtime-api/src/lib.rs | 13 +++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index 9c09ea3f22a9e..a72e46efe784d 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -47,7 +47,7 @@ pub(crate) struct RequestResultCache { check_validation_outputs: LruMap<(Hash, ParaId, CandidateCommitments), bool>, session_index_for_child: LruMap, validation_code: LruMap<(Hash, ParaId, OccupiedCoreAssumption), Option>, - validation_code_by_hash: LruMap>, + validation_code_by_hash: LruMap, candidate_pending_availability: LruMap<(Hash, ParaId), Option>, candidates_pending_availability: LruMap<(Hash, ParaId), Vec>, candidate_events: LruMap>, @@ -244,12 +244,15 @@ impl RequestResultCache { self.validation_code.insert(key, value); } - // the actual key is `ValidationCodeHash` (`Hash` is ignored), - // but we keep the interface that way to keep the macro simple + // The actual key is `ValidationCodeHash` (`Hash` is ignored). + // Only `Some` values are cached: validation code may not exist at query time but + // could be added later (e.g. via `force_set_current_code`). Caching `None` would + // produce stale results that prevent approval voting from fetching code that is + // already on-chain, stalling finality. pub(crate) fn validation_code_by_hash( &mut self, key: (Hash, ValidationCodeHash), - ) -> Option<&Option> { + ) -> Option<&ValidationCode> { self.validation_code_by_hash.get(&key.1).map(|v| &*v) } @@ -258,7 +261,9 @@ impl RequestResultCache { key: ValidationCodeHash, value: Option, ) { - self.validation_code_by_hash.insert(key, value); + if let Some(code) = value { + self.validation_code_by_hash.insert(key, code); + } } pub(crate) fn candidate_pending_availability( @@ -683,3 +688,28 @@ pub(crate) enum RequestResult { ParaIds(SessionIndex, Vec), UnappliedSlashesV2(Hash, Vec<(SessionIndex, CandidateHash, slashing::PendingSlashes)>), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validation_code_by_hash_does_not_cache_none() { + let mut cache = RequestResultCache::default(); + let relay_parent: Hash = [1u8; 32].into(); + let code = ValidationCode(vec![1, 2, 3]); + let code_hash = code.hash(); + + cache.cache_validation_code_by_hash(code_hash, None); + assert!( + cache.validation_code_by_hash((relay_parent, code_hash)).is_none(), + "None results must not be cached", + ); + + cache.cache_validation_code_by_hash(code_hash, Some(code.clone())); + assert_eq!( + cache.validation_code_by_hash((relay_parent, code_hash)), + Some(&code), + ); + } +} diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index c047fd46751c7..b9c4e9c7c5a01 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -292,8 +292,17 @@ where .map(|sender| Request::ValidationCode(para, assumption, sender)) }, Request::ValidationCodeByHash(validation_code_hash, sender) => { - query!(validation_code_by_hash(validation_code_hash), sender) - .map(|sender| Request::ValidationCodeByHash(validation_code_hash, sender)) + if let Some(code) = self + .requests_cache + .validation_code_by_hash((relay_parent.clone(), validation_code_hash)) + { + self.metrics.on_cached_request(); + let _ = sender.send(Ok(Some(code.clone()))); + None + } else { + Some(sender) + } + .map(|sender| Request::ValidationCodeByHash(validation_code_hash, sender)) }, Request::CandidatePendingAvailability(para, sender) => { query!(candidate_pending_availability(para), sender) From fb72f9f49455dd9275b2375bf270c516ac8e1f07 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:50:11 +0000 Subject: [PATCH 2/4] Update from github-actions[bot] running command 'prdoc --audience node_dev --bump patch' --- prdoc/pr_11108.prdoc | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 prdoc/pr_11108.prdoc diff --git a/prdoc/pr_11108.prdoc b/prdoc/pr_11108.prdoc new file mode 100644 index 0000000000000..4f5620fb3c659 --- /dev/null +++ b/prdoc/pr_11108.prdoc @@ -0,0 +1,8 @@ +title: 'polkadot-runtime-api-cache: Only cache validation code that exists' +doc: +- audience: Node Dev + description: |- + Otherwise there is the possibility that nodes cache `None` and some later `force_set_code` enacts the code. Then the nodes that have cached `None` do not know that the validation code now actually exists on chain. +crates: +- name: polkadot-node-core-runtime-api + bump: patch From ae2206cde5334649a2a50d6c557dfc736e1c5746 Mon Sep 17 00:00:00 2001 From: "cmd[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 09:31:08 +0000 Subject: [PATCH 3/4] Update from github-actions[bot] running command 'fmt' --- polkadot/node/core/runtime-api/src/cache.rs | 5 +---- polkadot/node/core/runtime-api/src/lib.rs | 24 ++++++++++----------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/polkadot/node/core/runtime-api/src/cache.rs b/polkadot/node/core/runtime-api/src/cache.rs index a72e46efe784d..30463e01a4659 100644 --- a/polkadot/node/core/runtime-api/src/cache.rs +++ b/polkadot/node/core/runtime-api/src/cache.rs @@ -707,9 +707,6 @@ mod tests { ); cache.cache_validation_code_by_hash(code_hash, Some(code.clone())); - assert_eq!( - cache.validation_code_by_hash((relay_parent, code_hash)), - Some(&code), - ); + assert_eq!(cache.validation_code_by_hash((relay_parent, code_hash)), Some(&code),); } } diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index b9c4e9c7c5a01..90084f3f4bada 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -291,19 +291,17 @@ where query!(validation_code(para, assumption), sender) .map(|sender| Request::ValidationCode(para, assumption, sender)) }, - Request::ValidationCodeByHash(validation_code_hash, sender) => { - if let Some(code) = self - .requests_cache - .validation_code_by_hash((relay_parent.clone(), validation_code_hash)) - { - self.metrics.on_cached_request(); - let _ = sender.send(Ok(Some(code.clone()))); - None - } else { - Some(sender) - } - .map(|sender| Request::ValidationCodeByHash(validation_code_hash, sender)) - }, + Request::ValidationCodeByHash(validation_code_hash, sender) => if let Some(code) = self + .requests_cache + .validation_code_by_hash((relay_parent.clone(), validation_code_hash)) + { + self.metrics.on_cached_request(); + let _ = sender.send(Ok(Some(code.clone()))); + None + } else { + Some(sender) + } + .map(|sender| Request::ValidationCodeByHash(validation_code_hash, sender)), Request::CandidatePendingAvailability(para, sender) => { query!(candidate_pending_availability(para), sender) .map(|sender| Request::CandidatePendingAvailability(para, sender)) From 8d781b2001afdfc14f7f2d3ce7748cc74faac4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20K=C3=B6cher?= Date: Thu, 19 Feb 2026 10:59:01 +0100 Subject: [PATCH 4/4] Apply suggestion from @bkchr --- polkadot/node/core/runtime-api/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polkadot/node/core/runtime-api/src/lib.rs b/polkadot/node/core/runtime-api/src/lib.rs index 90084f3f4bada..e12dbf646a35f 100644 --- a/polkadot/node/core/runtime-api/src/lib.rs +++ b/polkadot/node/core/runtime-api/src/lib.rs @@ -293,7 +293,7 @@ where }, Request::ValidationCodeByHash(validation_code_hash, sender) => if let Some(code) = self .requests_cache - .validation_code_by_hash((relay_parent.clone(), validation_code_hash)) + .validation_code_by_hash((relay_parent, validation_code_hash)) { self.metrics.on_cached_request(); let _ = sender.send(Ok(Some(code.clone())));