diff --git a/frame/beefy-mmr/src/tests.rs b/frame/beefy-mmr/src/tests.rs index b9851d9eef6cb..1826331f59e53 100644 --- a/frame/beefy-mmr/src/tests.rs +++ b/frame/beefy-mmr/src/tests.rs @@ -100,8 +100,8 @@ fn should_contain_mmr_digest() { #[test] fn should_contain_valid_leaf_data() { - fn node_offchain_key(parent_hash: H256, pos: usize) -> Vec { - (::INDEXING_PREFIX, parent_hash, pos as u64).encode() + fn node_offchain_key(pos: usize, parent_hash: H256) -> Vec { + (::INDEXING_PREFIX, pos as u64, parent_hash).encode() } let mut ext = new_test_ext(vec![1, 2, 3, 4]); @@ -110,7 +110,7 @@ fn should_contain_valid_leaf_data() { >::parent_hash() }); - let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(parent_hash, 0)); + let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(0, parent_hash)); assert_eq!( mmr_leaf, MmrLeaf { @@ -135,7 +135,7 @@ fn should_contain_valid_leaf_data() { >::parent_hash() }); - let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(parent_hash, 1)); + let mmr_leaf = read_mmr_leaf(&mut ext, node_offchain_key(1, parent_hash)); assert_eq!( mmr_leaf, MmrLeaf { diff --git a/frame/merkle-mountain-range/src/lib.rs b/frame/merkle-mountain-range/src/lib.rs index ad3ce340496e8..92ab8702b65c0 100644 --- a/frame/merkle-mountain-range/src/lib.rs +++ b/frame/merkle-mountain-range/src/lib.rs @@ -57,9 +57,9 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::Encode; -use frame_support::weights::Weight; +use frame_support::{log, traits::Get, weights::Weight}; use sp_runtime::{ - traits::{self, CheckedSub, One, Saturating}, + traits::{self, CheckedSub, One, Saturating, UniqueSaturatedInto}, SaturatedConversion, }; @@ -103,6 +103,15 @@ pub trait WeightInfo { fn on_initialize(peaks: NodeIndex) -> Weight; } +/// A MMR specific to the pallet. +type ModuleMmr = mmr::Mmr>; + +/// Leaf data. +type LeafOf = <>::LeafData as primitives::LeafDataProvider>::LeafData; + +/// Hashing used for the pallet. +pub(crate) type HashingOf = >::Hashing; + #[frame_support::pallet] pub mod pallet { use super::*; @@ -166,7 +175,7 @@ pub mod pallet { /// Note that the leaf at each block MUST be unique. You may want to include a block hash or /// block number as an easiest way to ensure that. /// Also note that the leaf added by each block is expected to only reference data coming - /// from ancestor blocks (leaves are saved offchain using `(parent_hash, pos)` key to be + /// from ancestor blocks (leaves are saved offchain using `(pos, parent_hash)` key to be /// fork-resistant, as such conflicts could only happen on 1-block deep forks, which means /// two forks with identical line of ancestors compete to write the same offchain key, but /// that's fine as long as leaves only contain data coming from ancestors - conflicting @@ -212,12 +221,22 @@ pub mod pallet { let leaves = Self::mmr_leaves(); let peaks_before = mmr::utils::NodesUtils::new(leaves).number_of_peaks(); let data = T::LeafData::leaf_data(); + // append new leaf to MMR let mut mmr: ModuleMmr = mmr::Mmr::new(leaves); - mmr.push(data).expect("MMR push never fails."); - - // update the size - let (leaves, root) = mmr.finalize().expect("MMR finalize never fails."); + // MMR push never fails, but better safe than sorry. + if mmr.push(data).is_none() { + log::error!(target: "runtime::mmr", "MMR push failed"); + return T::WeightInfo::on_initialize(peaks_before) + } + // Update the size, `mmr.finalize()` should also never fail. + let (leaves, root) = match mmr.finalize() { + Ok((leaves, root)) => (leaves, root), + Err(e) => { + log::error!(target: "runtime::mmr", "MMR finalize failed: {:?}", e); + return T::WeightInfo::on_initialize(peaks_before) + }, + }; >::on_new_root(&root); >::put(leaves); @@ -230,21 +249,35 @@ pub mod pallet { fn offchain_worker(n: T::BlockNumber) { use mmr::storage::{OffchainStorage, Storage}; - // MMR pallet uses offchain storage to hold full MMR and leaves. - // The leaves are saved under fork-unique keys `(parent_hash, pos)`. - // MMR Runtime depends on `frame_system::block_hash(block_num)` mappings to find - // parent hashes for particular nodes or leaves. - // This MMR offchain worker function moves a rolling window of the same size - // as `frame_system::block_hash` map, where nodes/leaves added by blocks that are just + // The MMR nodes can be found in offchain db under either: + // - fork-unique keys `(prefix, pos, parent_hash)`, or, + // - "canonical" keys `(prefix, pos)`, + // depending on how many blocks in the past the node at position `pos` was + // added to the MMR. + // + // For the fork-unique keys, the MMR pallet depends on + // `frame_system::block_hash(parent_num)` mappings to find the relevant parent block + // hashes, so it is limited by `frame_system::BlockHashCount` in terms of how many + // historical forks it can track. Nodes added to MMR by block `N` can be found in + // offchain db at: + // - fork-unique keys `(prefix, pos, parent_hash)` when (`N` >= `latest_block` - + // `frame_system::BlockHashCount`); + // - "canonical" keys `(prefix, pos)` when (`N` < `latest_block` - + // `frame_system::BlockHashCount`); + // + // The offchain worker is responsible for maintaining the nodes' positions in + // offchain db as the chain progresses by moving a rolling window of the same size as + // `frame_system::block_hash` map, where nodes/leaves added by blocks that are just // about to exit the window are "canonicalized" so that their offchain key no longer - // depends on `parent_hash` therefore on access to `frame_system::block_hash`. + // depends on `parent_hash`. // // This approach works to eliminate fork-induced leaf collisions in offchain db, // under the assumption that no fork will be deeper than `frame_system::BlockHashCount` - // blocks (2400 blocks on Polkadot, Kusama, Rococo, etc): - // entries pertaining to block `N` where `N < current-2400` are moved to a key based - // solely on block number. The only way to have collisions is if two competing forks - // are deeper than 2400 blocks and they both "canonicalize" their view of block `N`. + // blocks: + // entries pertaining to block `N` where `N < current-BlockHashCount` are moved to a + // key based solely on block number. The only way to have collisions is if two + // competing forks are deeper than `frame_system::BlockHashCount` blocks and they + // both "canonicalize" their view of block `N` // Once a block is canonicalized, all MMR entries pertaining to sibling blocks from // other forks are pruned from offchain db. Storage::>::canonicalize_and_prune(n); @@ -252,15 +285,6 @@ pub mod pallet { } } -/// A MMR specific to the pallet. -type ModuleMmr = mmr::Mmr>; - -/// Leaf data. -type LeafOf = <>::LeafData as primitives::LeafDataProvider>::LeafData; - -/// Hashing used for the pallet. -pub(crate) type HashingOf = >::Hashing; - /// Stateless MMR proof verification for batch of leaves. /// /// This function can be used to verify received MMR [primitives::BatchProof] (`proof`) @@ -290,19 +314,32 @@ impl, I: 'static> Pallet { /// /// This combination makes the offchain (key,value) entry resilient to chain forks. fn node_offchain_key( - parent_hash: ::Hash, pos: NodeIndex, + parent_hash: ::Hash, ) -> sp_std::prelude::Vec { - (T::INDEXING_PREFIX, parent_hash, pos).encode() + (T::INDEXING_PREFIX, pos, parent_hash).encode() } /// Build canonical offchain key for node `pos` in MMR. /// /// Used for nodes added by now finalized blocks. + /// Never read keys using `node_canon_offchain_key` unless you sure that + /// there's no `node_offchain_key` key in the storage. fn node_canon_offchain_key(pos: NodeIndex) -> sp_std::prelude::Vec { (T::INDEXING_PREFIX, pos).encode() } + /// Return size of rolling window of leaves saved in offchain under fork-unique keys. + /// + /// Leaves outside this window are canonicalized. + /// Window size is `frame_system::BlockHashCount - 1` to make sure fork-unique keys + /// can be built using `frame_system::block_hash` map. + fn offchain_canonicalization_window() -> LeafIndex { + let window_size: LeafIndex = + ::BlockHashCount::get().unique_saturated_into(); + window_size.saturating_sub(1) + } + /// Provide the parent number for the block that added `leaf_index` to the MMR. fn leaf_index_to_parent_block_num( leaf_index: LeafIndex, diff --git a/frame/merkle-mountain-range/src/mmr/storage.rs b/frame/merkle-mountain-range/src/mmr/storage.rs index 8b623edf56957..870ce81226bd2 100644 --- a/frame/merkle-mountain-range/src/mmr/storage.rs +++ b/frame/merkle-mountain-range/src/mmr/storage.rs @@ -18,11 +18,10 @@ //! A MMR storage implementations. use codec::Encode; -use frame_support::traits::Get; +use frame_support::log::{debug, error, trace}; use mmr_lib::helper; use sp_core::offchain::StorageKind; use sp_io::{offchain, offchain_index}; -use sp_runtime::traits::UniqueSaturatedInto; use sp_std::iter::Peekable; #[cfg(not(feature = "std"))] use sp_std::prelude::*; @@ -133,15 +132,14 @@ where // Effectively move a rolling window of fork-unique leaves. Once out of the window, leaves // are "canonicalized" in offchain by moving them under `Pallet::node_canon_offchain_key`. let leaves = NumberOfLeaves::::get(); - let window_size = - ::BlockHashCount::get().unique_saturated_into(); + let window_size = Pallet::::offchain_canonicalization_window(); if leaves >= window_size { // Move the rolling window towards the end of `block_num->hash` mappings available // in the runtime: we "canonicalize" the leaf at the end, let to_canon_leaf = leaves.saturating_sub(window_size); // and all the nodes added by that leaf. let to_canon_nodes = NodesUtils::right_branch_ending_in_leaf(to_canon_leaf); - frame_support::log::debug!( + debug!( target: "runtime::mmr::offchain", "Nodes to canon for leaf {}: {:?}", to_canon_leaf, to_canon_nodes ); @@ -149,7 +147,7 @@ where let to_canon_block_num = Pallet::::leaf_index_to_parent_block_num(to_canon_leaf, leaves); // Only entries under this hash (retrieved from state on current canon fork) are to be - // persisted. All other entries added by same block number will be cleared. + // persisted. All entries added by same block number on other forks will be cleared. let to_canon_hash = >::block_hash(to_canon_block_num); Self::canonicalize_nodes_for_hash(&to_canon_nodes, to_canon_hash); @@ -159,7 +157,7 @@ where Self::prune_nodes_for_forks(&to_canon_nodes, forks); }) .unwrap_or_else(|| { - frame_support::log::error!( + error!( target: "runtime::mmr::offchain", "Offchain: could not prune: no entry in pruning map for block {:?}", to_canon_block_num @@ -171,8 +169,8 @@ where fn prune_nodes_for_forks(nodes: &[NodeIndex], forks: Vec<::Hash>) { for hash in forks { for pos in nodes { - let key = Pallet::::node_offchain_key(hash, *pos); - frame_support::log::debug!( + let key = Pallet::::node_offchain_key(*pos, hash); + debug!( target: "runtime::mmr::offchain", "Clear elem at pos {} with key {:?}", pos, key @@ -187,19 +185,19 @@ where to_canon_hash: ::Hash, ) { for pos in to_canon_nodes { - let key = Pallet::::node_offchain_key(to_canon_hash, *pos); + let key = Pallet::::node_offchain_key(*pos, to_canon_hash); // Retrieve the element from Off-chain DB under fork-aware key. if let Some(elem) = offchain::local_storage_get(StorageKind::PERSISTENT, &key) { let canon_key = Pallet::::node_canon_offchain_key(*pos); // Add under new canon key. offchain::local_storage_set(StorageKind::PERSISTENT, &canon_key, &elem); - frame_support::log::debug!( + debug!( target: "runtime::mmr::offchain", "Moved elem at pos {} from key {:?} to canon key {:?}", pos, key, canon_key ); } else { - frame_support::log::error!( + error!( target: "runtime::mmr::offchain", "Could not canonicalize elem at pos {} using key {:?}", pos, key @@ -220,21 +218,18 @@ where // Find out which leaf added node `pos` in the MMR. let ancestor_leaf_idx = NodesUtils::leaf_index_that_added_node(pos); - let window_size = - ::BlockHashCount::get().unique_saturated_into(); + let window_size = Pallet::::offchain_canonicalization_window(); // Leaves older than this window should have been canonicalized. if leaves.saturating_sub(ancestor_leaf_idx) > window_size { let key = Pallet::::node_canon_offchain_key(pos); - frame_support::log::debug!( + debug!( target: "runtime::mmr::offchain", "offchain db get {}: leaf idx {:?}, key {:?}", pos, ancestor_leaf_idx, key ); // Just for safety, to easily handle runtime upgrades where any of the window params // change and maybe we mess up storage migration, // return _if and only if_ node is found (in normal conditions it's always found), - if let Some(elem) = - sp_io::offchain::local_storage_get(sp_core::offchain::StorageKind::PERSISTENT, &key) - { + if let Some(elem) = sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, &key) { return Ok(codec::Decode::decode(&mut &*elem).ok()) } // BUT if we DID MESS UP, fall through to searching node using fork-specific key. @@ -244,20 +239,20 @@ where let ancestor_parent_block_num = Pallet::::leaf_index_to_parent_block_num(ancestor_leaf_idx, leaves); let ancestor_parent_hash = >::block_hash(ancestor_parent_block_num); - let key = Pallet::::node_offchain_key(ancestor_parent_hash, pos); - frame_support::log::debug!( + let key = Pallet::::node_offchain_key(pos, ancestor_parent_hash); + debug!( target: "runtime::mmr::offchain", "offchain db get {}: leaf idx {:?}, hash {:?}, key {:?}", pos, ancestor_leaf_idx, ancestor_parent_hash, key ); // Retrieve the element from Off-chain DB. - Ok(sp_io::offchain::local_storage_get(sp_core::offchain::StorageKind::PERSISTENT, &key) + Ok(sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, &key) .or_else(|| { // Again, this is just us being extra paranoid. // We get here only if we mess up a storage migration for a runtime upgrades where // say the window is increased, and for a little while following the upgrade there's // leaves inside new 'window' that had been already canonicalized before upgrade. let key = Pallet::::node_canon_offchain_key(pos); - sp_io::offchain::local_storage_get(sp_core::offchain::StorageKind::PERSISTENT, &key) + sp_io::offchain::local_storage_get(StorageKind::PERSISTENT, &key) }) .and_then(|v| codec::Decode::decode(&mut &*v).ok())) } @@ -282,9 +277,8 @@ where return Ok(()) } - frame_support::log::trace!( - target: "runtime::mmr", - "elems: {:?}", + trace!( + target: "runtime::mmr", "elems: {:?}", elems.iter().map(|elem| elem.hash()).collect::>() ); @@ -309,25 +303,12 @@ where // in offchain DB to avoid DB collisions and overwrites in case of forks. let parent_hash = >::parent_hash(); for elem in elems { - // For now we store this leaf offchain keyed by `(parent_hash, node_index)` - // to make it fork-resistant. - // Offchain worker task will "canonicalize" it `frame_system::BlockHashCount` blocks - // later when we are not worried about forks anymore (highly unlikely to have a fork - // in the chain that deep). - // "Canonicalization" in this case means moving this leaf under a new key based - // only on the leaf's `node_index`. - let key = Pallet::::node_offchain_key(parent_hash, node_index); - frame_support::log::debug!( - target: "runtime::mmr::offchain", "offchain db set: pos {} parent_hash {:?} key {:?}", - node_index, parent_hash, key - ); - // Indexing API is used to store the full node content (both leaf and inner). - elem.using_encoded(|elem| offchain_index::set(&key, elem)); - // On-chain we are going to only store new peaks. if peaks_to_store.next_if_eq(&node_index).is_some() { >::insert(node_index, elem.hash()); } + // We are storing full node off-chain (using indexing API). + Self::store_to_offchain(node_index, parent_hash, &elem); // Increase the indices. if let Node::Data(..) = elem { @@ -348,6 +329,38 @@ where } } +impl Storage +where + T: Config, + I: 'static, + L: primitives::FullLeaf, +{ + fn store_to_offchain( + pos: NodeIndex, + parent_hash: ::Hash, + node: &NodeOf, + ) { + let encoded_node = node.encode(); + // We store this leaf offchain keyed by `(parent_hash, node_index)` to make it + // fork-resistant. Offchain worker task will "canonicalize" it + // `frame_system::BlockHashCount` blocks later, when we are not worried about forks anymore + // (multi-era-deep forks should not happen). + let key = Pallet::::node_offchain_key(pos, parent_hash); + debug!( + target: "runtime::mmr::offchain", "offchain db set: pos {} parent_hash {:?} key {:?}", + pos, parent_hash, key + ); + // Indexing API is used to store the full node content. + offchain_index::set(&key, &encoded_node); + // We also directly save the full node under the "canonical" key. + // This is superfluous for the normal case - this entry will possibly be overwritten + // by forks, and will also be overwritten by "offchain_worker canonicalization". + // But it is required for blocks imported during initial sync where none of the above apply + // (`offchain_worker` doesn't run for initial sync blocks). + offchain_index::set(&Pallet::::node_canon_offchain_key(pos), &encoded_node); + } +} + fn peaks_to_prune_and_store( old_size: NodeIndex, new_size: NodeIndex, @@ -356,8 +369,8 @@ fn peaks_to_prune_and_store( // both collections may share a common prefix. let peaks_before = if old_size == 0 { vec![] } else { helper::get_peaks(old_size) }; let peaks_after = helper::get_peaks(new_size); - frame_support::log::trace!(target: "runtime::mmr", "peaks_before: {:?}", peaks_before); - frame_support::log::trace!(target: "runtime::mmr", "peaks_after: {:?}", peaks_after); + trace!(target: "runtime::mmr", "peaks_before: {:?}", peaks_before); + trace!(target: "runtime::mmr", "peaks_after: {:?}", peaks_after); let mut peaks_before = peaks_before.into_iter().peekable(); let mut peaks_after = peaks_after.into_iter().peekable(); diff --git a/frame/merkle-mountain-range/src/tests.rs b/frame/merkle-mountain-range/src/tests.rs index a63a433029295..dbbdc12c8e1d5 100644 --- a/frame/merkle-mountain-range/src/tests.rs +++ b/frame/merkle-mountain-range/src/tests.rs @@ -169,25 +169,22 @@ fn should_append_to_mmr_when_on_initialize_is_called() { ext.persist_offchain_overlay(); let offchain_db = ext.offchain_db(); - assert_eq!( - offchain_db.get(&MMR::node_offchain_key(parent_b1, 0)).map(decode_node), - Some(mmr::Node::Data(((0, H256::repeat_byte(1)), LeafData::new(1),))) - ); - assert_eq!( - offchain_db.get(&MMR::node_offchain_key(parent_b2, 1)).map(decode_node), - Some(mmr::Node::Data(((1, H256::repeat_byte(2)), LeafData::new(2),))) - ); - assert_eq!( - offchain_db.get(&MMR::node_offchain_key(parent_b2, 2)).map(decode_node), - Some(mmr::Node::Hash(hex( - "672c04a9cd05a644789d769daa552d35d8de7c33129f8a7cbf49e595234c4854" - ))) - ); - assert_eq!(offchain_db.get(&MMR::node_offchain_key(parent_b2, 3)), None); - assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(0)), None); - assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(1)), None); - assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(2)), None); + let expected = Some(mmr::Node::Data(((0, H256::repeat_byte(1)), LeafData::new(1)))); + assert_eq!(offchain_db.get(&MMR::node_offchain_key(0, parent_b1)).map(decode_node), expected); + assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(0)).map(decode_node), expected); + + let expected = Some(mmr::Node::Data(((1, H256::repeat_byte(2)), LeafData::new(2)))); + assert_eq!(offchain_db.get(&MMR::node_offchain_key(1, parent_b2)).map(decode_node), expected); + assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(1)).map(decode_node), expected); + + let expected = Some(mmr::Node::Hash(hex( + "672c04a9cd05a644789d769daa552d35d8de7c33129f8a7cbf49e595234c4854", + ))); + assert_eq!(offchain_db.get(&MMR::node_offchain_key(2, parent_b2)).map(decode_node), expected); + assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(2)).map(decode_node), expected); + + assert_eq!(offchain_db.get(&MMR::node_offchain_key(3, parent_b2)), None); assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(3)), None); } @@ -815,16 +812,20 @@ fn should_canonicalize_offchain() { let parent_num: BlockNumber = (block_num - 1).into(); let leaf_index = u64::from(block_num - 1); let pos = helper::leaf_index_to_pos(leaf_index.into()); - // not canon, - assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(pos)), None); let parent_hash = >::block_hash(parent_num); - // but available in fork-proof storage. + // Available in offchain db under both fork-proof key and canon key. + // We'll later check it is pruned from fork-proof key. + let expected = Some(mmr::Node::Data(( + (leaf_index, H256::repeat_byte(u8::try_from(block_num).unwrap())), + LeafData::new(block_num.into()), + ))); assert_eq!( - offchain_db.get(&MMR::node_offchain_key(parent_hash, pos)).map(decode_node), - Some(mmr::Node::Data(( - (leaf_index, H256::repeat_byte(u8::try_from(block_num).unwrap())), - LeafData::new(block_num.into()), - ))) + offchain_db.get(&MMR::node_canon_offchain_key(pos)).map(decode_node), + expected + ); + assert_eq!( + offchain_db.get(&MMR::node_offchain_key(pos, parent_hash)).map(decode_node), + expected ); } @@ -835,12 +836,16 @@ fn should_canonicalize_offchain() { let verify = |pos: NodeIndex, leaf_index: LeafIndex, expected: H256| { let parent_num: BlockNumber = leaf_index.try_into().unwrap(); let parent_hash = >::block_hash(parent_num); - // not canon, - assert_eq!(offchain_db.get(&MMR::node_canon_offchain_key(pos)), None); - // but available in fork-proof storage. + // Available in offchain db under both fork-proof key and canon key. + // We'll later check it is pruned from fork-proof key. + let expected = Some(mmr::Node::Hash(expected)); assert_eq!( - offchain_db.get(&MMR::node_offchain_key(parent_hash, pos)).map(decode_node), - Some(mmr::Node::Hash(expected)) + offchain_db.get(&MMR::node_canon_offchain_key(pos)).map(decode_node), + expected + ); + assert_eq!( + offchain_db.get(&MMR::node_offchain_key(pos, parent_hash)).map(decode_node), + expected ); }; verify(2, 1, hex("672c04a9cd05a644789d769daa552d35d8de7c33129f8a7cbf49e595234c4854")); @@ -867,7 +872,7 @@ fn should_canonicalize_offchain() { let parent_num: BlockNumber = (block_num - 1).into(); let parent_hash = >::block_hash(parent_num); // no longer available in fork-proof storage (was pruned), - assert_eq!(offchain_db.get(&MMR::node_offchain_key(parent_hash, pos)), None); + assert_eq!(offchain_db.get(&MMR::node_offchain_key(pos, parent_hash)), None); // but available using canon key. assert_eq!( offchain_db.get(&MMR::node_canon_offchain_key(pos)).map(decode_node), @@ -886,7 +891,7 @@ fn should_canonicalize_offchain() { let parent_num: BlockNumber = leaf_index.try_into().unwrap(); let parent_hash = >::block_hash(parent_num); // no longer available in fork-proof storage (was pruned), - assert_eq!(offchain_db.get(&MMR::node_offchain_key(parent_hash, pos)), None); + assert_eq!(offchain_db.get(&MMR::node_offchain_key(pos, parent_hash)), None); // but available using canon key. assert_eq!( offchain_db.get(&MMR::node_canon_offchain_key(pos)).map(decode_node),