Repository: iFrostizz/snowflake
Pull request #44: Implement light methods to the RPC
Author: login: iFrostizz
Created: 2025-05-02T06:41:11Z | Merged: 2025-05-06T06:24:30Z

Closes https://github.com/iFrostizz/snowflake/issues/41


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 8ee7433..3cfa5a1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1816,6 +1816,12 @@ dependencies = [
  "thousands",
 ]
 
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -3216,6 +3222,7 @@ dependencies = [
  "openssl",
  "paste",
  "phf",
+ "pretty_assertions",
  "prost",
  "proto",
  "public-ip",
@@ -3227,6 +3234,7 @@ dependencies = [
  "rustls-pemfile",
  "rustls-pki-types",
  "serde",
+ "serde-big-array",
  "serde_json",
  "sha2",
  "sha256",
@@ -3607,6 +3615,16 @@ dependencies = [
  "zerocopy 0.8.24",
 ]
 
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
 [[package]]
 name = "prettyplease"
 version = "0.2.31"
@@ -4352,6 +4370,15 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-big-array"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_derive"
 version = "1.0.219"
@@ -5733,6 +5760,12 @@ dependencies = [
  "tap",
 ]
 
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
 [[package]]
 name = "yoke"
 version = "0.7.5"
diff --git a/node/Cargo.toml b/node/Cargo.toml
index 47ef761..beb7ed1 100644
--- a/node/Cargo.toml
+++ b/node/Cargo.toml
@@ -50,8 +50,11 @@ current_platform = "0.2.0"
 ruint = "1.14.0"
 sha256 = "1.6.0"
 unsigned-varint = "0.8.0"
+serde-big-array = "0.5.1"
+pretty_assertions = "1.4.1"
 
 [dev-dependencies]
+pretty_assertions = "1.4.1"
 #criterion = "0.3"
 
 # [[bench]]
diff --git a/node/src/bin.rs b/node/src/bin.rs
index c2f2113..a41256c 100644
--- a/node/src/bin.rs
+++ b/node/src/bin.rs
@@ -66,6 +66,7 @@ async fn main() -> Result<(), NodeError> {
         args.max_latency_records,
         args.sync_headers,
     ));
+    node.light_network.block_dht.todo_attach_node(node.clone());
 
     let (node_tx, node_ops, server) = server(node.clone(), listener, &args).await;
 
diff --git a/node/src/blocks/block_store.rs b/node/src/blocks/block_store.rs
index 8ddd58c..09a943a 100644
--- a/node/src/blocks/block_store.rs
+++ b/node/src/blocks/block_store.rs
@@ -18,7 +18,7 @@ impl BlockStore {
     }
 
     pub fn push(&mut self, block_id: BlockID, height: u64) {
-        if self.store.insert(block_id.clone(), height).is_none() {
+        if self.store.insert(block_id, height).is_none() {
             if let Some(block_id) = self.cache.push(block_id) {
                 self.store.remove(&block_id);
             }
diff --git a/node/src/blocks/frontier.rs b/node/src/blocks/frontier.rs
index 1337ee8..abe7dff 100644
--- a/node/src/blocks/frontier.rs
+++ b/node/src/blocks/frontier.rs
@@ -35,7 +35,7 @@ impl Frontier {
                     (BlockID::default(), 0),
                     |(block_id, height), (new_block_id, new_height)| {
                         if new_height >= &height {
-                            (new_block_id.clone(), *new_height)
+                            (*new_block_id, *new_height)
                         } else {
                             (block_id, height)
                         }
@@ -47,7 +47,7 @@ impl Frontier {
 
     pub fn push(&mut self, block_id: BlockID) {
         if !self.has_block(&block_id) {
-            if let Some(rem_block_id) = self.blocks.push(block_id.clone()) {
+            if let Some(rem_block_id) = self.blocks.push(block_id) {
                 self.set.remove(&rem_block_id);
             }
             self.set.insert(block_id);
@@ -69,10 +69,10 @@ mod tests {
         let mut front = Frontier::new(5);
         assert!(front.tip(&BlockStore::new(1)).is_none());
         let block_id = BlockID::from([1; 32]);
-        front.push(block_id.clone());
+        front.push(block_id);
         assert!(front.tip(&BlockStore::new(1)).is_none());
         let mut store = BlockStore::new(1);
-        store.push(block_id.clone(), 0);
+        store.push(block_id, 0);
         assert_eq!(front.tip(&store), Some((block_id, 0)));
     }
 }
diff --git a/node/src/cli.rs b/node/src/cli.rs
index 2b920f1..70f2af6 100644
--- a/node/src/cli.rs
+++ b/node/src/cli.rs
@@ -156,7 +156,7 @@ impl Args {
         let network = &self.network_id.to_string();
         let network_id = constants::NETWORK[network];
         let eth_network_id = constants::ETH_NETWORK[network];
-        let c_chain_id: ChainId = constants::C_CHAIN_ID[network].clone();
+        let c_chain_id: ChainId = constants::C_CHAIN_ID[network];
 
         let socket_addr = match self.public_ip.unwrap() {
             IpAddr::V4(ip) => SocketAddr::new(IpAddr::V4(ip), self.http_port),
diff --git a/node/src/client/bootstrap.rs b/node/src/client/bootstrap.rs
index 3456856..c1eca65 100644
--- a/node/src/client/bootstrap.rs
+++ b/node/src/client/bootstrap.rs
@@ -2,7 +2,7 @@ use crate::dht::DhtBuckets;
 use crate::id::NodeId;
 use crate::net::{node::NodeError, queue::ConnectionData};
 use crate::node::Node;
-use serde::{Deserialize, Deserializer};
+use serde::Deserialize;
 use std::collections::HashMap;
 use std::fs::File;
 use std::io;
@@ -13,20 +13,10 @@ use std::sync::Arc;
 
 #[derive(Deserialize, Debug)]
 pub struct Bootstrapper {
-    #[serde(deserialize_with = "string_to_node_id")]
     pub id: NodeId,
     pub ip: SocketAddr,
 }
 
-fn string_to_node_id<'de, D>(de: D) -> Result<NodeId, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let node_id_string = <String>::deserialize(de)?;
-    let node_id = NodeId::try_from(node_id_string.as_str()).map_err(serde::de::Error::custom)?;
-    Ok(node_id)
-}
-
 pub struct Bootstrappers<'a> {
     pub bootstrapper_path: &'a Path,
     pub light_bootstrapper_path: &'a Path,
diff --git a/node/src/dht/block.rs b/node/src/dht/block.rs
index b5e1289..881abf3 100644
--- a/node/src/dht/block.rs
+++ b/node/src/dht/block.rs
@@ -1,22 +1,31 @@
 use crate::dht::kademlia::LockedMapDb;
-use crate::dht::{Bucket, DhtId};
+use crate::dht::{light_errors, Bucket, DhtId, LightError};
 use crate::dht::{ConcreteDht, Dht};
-use crate::id::NodeId;
+use crate::id::{BlockID, NodeId};
 use crate::message::SubscribableMessage;
+use crate::net::light::DhtContent;
+use crate::net::Network;
 use crate::node::{MessageOrSubscribable, Node, SinglePickerConfig};
 use crate::utils::constants::DEFAULT_DEADLINE;
 use crate::utils::unpacker::StatelessBlock;
+use crate::utils::FIFOSet;
 use crate::Arc;
 use alloy::primitives::keccak256;
 use proto_lib::p2p::message::Message;
 use proto_lib::p2p::GetAcceptedFrontier;
+use proto_lib::p2p::{Accepted, GetAccepted};
 use proto_lib::p2p::{EngineType, GetAncestors};
 use std::collections::HashMap;
-use std::sync::RwLock;
+use std::sync::{Mutex, RwLock};
 use std::time::Duration;
 use tokio::sync::broadcast;
 
-pub type DhtBlocks = Dht<RwLock<HashMap<Bucket, Vec<u8>>>>;
+#[derive(Debug)]
+pub struct DhtBlocks {
+    node: Mutex<Option<Arc<Node>>>,
+    pub dht: Dht<RwLock<HashMap<Bucket, Vec<u8>>>>,
+    pub verified_blocks: RwLock<FIFOSet<BlockID>>,
+}
 
 impl ConcreteDht<u64> for DhtBlocks {
     fn id() -> DhtId {
@@ -31,11 +40,104 @@ impl ConcreteDht<u64> for DhtBlocks {
     }
 }
 
+impl DhtContent<u64, StatelessBlock> for DhtBlocks {
+    fn get_from_store(&self, bucket: &Bucket) -> Result<Option<StatelessBlock>, LightError> {
+        match self.dht.store.get(bucket) {
+            Some(block_bytes) => {
+                let block = Self::decode(&block_bytes)?;
+                Ok(Some(block))
+            }
+            None => Ok(None),
+        }
+    }
+
+    async fn insert_to_store(&self, bytes: Vec<u8>) -> Result<Option<StatelessBlock>, LightError> {
+        let decoded = Self::decode(&bytes)?;
+        let number = u64::from_be_bytes(*decoded.block.header.number());
+        let bucket = Self::key_to_bucket(number);
+        if !self.dht.is_desired_bucket(&bucket) {
+            return Err(light_errors::UNDESIRED_BUCKET);
+        }
+        if !self.verify(&decoded).await? {
+            return Err(light_errors::INVALID_CONTENT);
+        }
+        match self.dht.store.insert(bucket, bytes) {
+            Some(ret_bytes) => Ok(Some(Self::decode(&ret_bytes)?)),
+            None => Ok(None),
+        }
+    }
+
+    async fn verify(&self, block: &StatelessBlock) -> Result<bool, LightError> {
+        let number = u64::from_be_bytes(*block.block.header.number());
+        let bucket = Self::key_to_bucket(number);
+        if self.get_from_store(&bucket)?.is_some() {
+            return Ok(true);
+        }
+        if self.verified_blocks.read().unwrap().contains(block.id()) {
+            return Ok(true);
+        }
+        let node = self.node.lock().unwrap().clone().unwrap();
+        let bootstrapper = Self::pick_random_bootstrapper(&node.network).await;
+        let message = SubscribableMessage::GetAccepted(GetAccepted {
+            chain_id: node.network.config.c_chain_id.as_ref().to_vec(),
+            request_id: rand::random(),
+            deadline: DEFAULT_DEADLINE,
+            container_ids: vec![block.id().as_ref().to_vec()],
+        });
+        let res = node
+            .send_to_peer(
+                &MessageOrSubscribable::Subscribable(message.clone()),
+                bootstrapper,
+            )
+            .await;
+        if let Some(Message::Accepted(Accepted {
+            chain_id,
+            request_id: _request_id,
+            container_ids,
+        })) = res
+        {
+            if chain_id != node.network.config.c_chain_id.as_ref().to_vec() {
+                return Err(light_errors::INVALID_CONTENT);
+            }
+            if container_ids == vec![block.id().as_ref().to_vec()] {
+                self.verified_blocks.write().unwrap().insert(*block.id());
+                Ok(true)
+            } else {
+                Ok(false)
+            }
+        } else {
+            Err(light_errors::INVALID_CONTENT)
+        }
+    }
+
+    fn encode(value: StatelessBlock) -> Result<Vec<u8>, LightError> {
+        value.pack().map_err(|_| light_errors::ENCODING_FAILED)
+    }
+
+    fn decode(bytes: &[u8]) -> Result<StatelessBlock, LightError> {
+        StatelessBlock::unpack(bytes).map_err(|_| light_errors::DECODING_FAILED)
+    }
+}
+
 impl DhtBlocks {
+    pub(crate) fn new(node_id: NodeId) -> Self {
+        let store = RwLock::new(HashMap::new());
+        Self {
+            node: Mutex::new(None),
+            dht: Dht::new(node_id, Bucket::from(10), store),
+            verified_blocks: RwLock::new(FIFOSet::new(10000)),
+        }
+    }
+
+    pub fn todo_attach_node(&self, node: Arc<Node>) {
+        *self.node.lock().unwrap() = Some(node);
+    }
+
+    // TODO here me mostly need the network. Rewrite this function.
     async fn sync_process(self: Arc<Self>, node: Arc<Node>) {
         let chain_id = node.network.config.c_chain_id.as_ref().to_vec();
         // TODO instead of a random bootstrapper, we should pick them in a loop.
-        let mut bootstrapper = Self::pick_random_bootstrapper(&node).await;
+        let mut bootstrapper = Self::pick_random_bootstrapper(&node.network).await;
 
         let message = SubscribableMessage::GetAcceptedFrontier(GetAcceptedFrontier {
             chain_id: chain_id.clone(),
@@ -81,11 +183,13 @@ impl DhtBlocks {
             for (i, container) in message.containers.into_iter().enumerate() {
                 let block = StatelessBlock::unpack(&container).unwrap();
                 if i == len - 1 {
-                    last_container_id = block.id.as_ref().to_vec();
+                    last_container_id = block.id().as_ref().to_vec();
                 }
                 let number = u64::from_be_bytes(*block.block.header.number());
-                self.store.insert(Self::key_to_bucket(number), container);
-                bootstrapper = Self::pick_random_bootstrapper(&node).await;
+                self.dht
+                    .store
+                    .insert(Self::key_to_bucket(number), container);
+                bootstrapper = Self::pick_random_bootstrapper(&node.network).await;
             }
         }
     }
@@ -99,11 +203,19 @@ impl DhtBlocks {
         }
     }
 
-    async fn pick_random_bootstrapper(node: &Arc<Node>) -> NodeId {
-        let mut maybe_bootstrapper = node.pick_peer(SinglePickerConfig::Bootstrapper);
+    async fn pick_random_bootstrapper(network: &Arc<Network>) -> NodeId {
+        let mut maybe_bootstrapper = Network::pick_peer(
+            &network.peers_infos,
+            &network.bootstrappers,
+            SinglePickerConfig::Bootstrapper,
+        );
         while maybe_bootstrapper.is_none() {
             tokio::time::sleep(Duration::from_millis(100)).await;
-            maybe_bootstrapper = node.pick_peer(SinglePickerConfig::Bootstrapper);
+            maybe_bootstrapper = Network::pick_peer(
+                &network.peers_infos,
+                &network.bootstrappers,
+                SinglePickerConfig::Bootstrapper,
+            );
         }
         maybe_bootstrapper.unwrap()
     }
diff --git a/node/src/dht/kademlia.rs b/node/src/dht/kademlia.rs
index 4c4a383..28a6817 100644
--- a/node/src/dht/kademlia.rs
+++ b/node/src/dht/kademlia.rs
@@ -3,12 +3,10 @@ use crate::id::{ChainId, NodeId};
 use crate::message::mail_box::Mail;
 use crate::message::SubscribableMessage;
 use crate::net::light::LightPeers;
-use crate::net::node::NodeError;
 use crate::net::queue::ConnectionData;
-use crate::server::msg::InboundMessageExt;
-use crate::server::msg::{AppRequestMessage, InboundMessage};
+use crate::server::msg::AppRequestMessage;
 use crate::server::peers::{PeerInfo, PeerSender};
-use crate::utils::constants::SNOWFLAKE_HANDLER_ID;
+use crate::utils::constants;
 use flume::Sender;
 use indexmap::IndexMap;
 use proto_lib::{p2p, sdk};
@@ -38,10 +36,10 @@ where
 
 #[derive(Debug)]
 pub struct KademliaDht {
-    peers_infos: Arc<RwLock<IndexMap<NodeId, PeerInfo>>>,
+    pub peers_infos: Arc<RwLock<IndexMap<NodeId, PeerInfo>>>,
     light_peers: LightPeers,
-    mail_tx: Sender<Mail>,
-    chain_id: ChainId,
+    pub mail_tx: Sender<Mail>,
+    pub chain_id: ChainId,
     /// Maximum number of nodes to return in a `find_node` request.
     max_nodes: usize,
 }
@@ -153,6 +151,7 @@ impl KademliaDht {
         nodes_with_content
     }
 
+    /// Lookup locally for nodes spanning the bucket.
     /// Find up to `n` unique nodes that are the closest to the `bucket`.
     pub fn find_node(&self, bucket: &Bucket) -> Vec<ConnectionData> {
         let nodes = self.light_peers.read().unwrap();
@@ -218,6 +217,30 @@ impl KademliaDht {
         Err(light_errors::CONTENT_NOT_FOUND)
     }
 
+    pub async fn store(
+        &self,
+        node_id: NodeId,
+        dht_id: &DhtId,
+        value: Vec<u8>,
+    ) -> Result<(), LightError> {
+        let sender = self
+            .peers_infos
+            .read()
+            .unwrap()
+            .get(&node_id)
+            .map(|infos| infos.sender.tx.clone())
+            .ok_or(light_errors::PEER_MISSING)?;
+        let message = AppRequestMessage::encode(
+            &self.chain_id,
+            sdk::Store {
+                dht_id: dht_id.into(),
+                value,
+            },
+        )
+        .unwrap();
+        sender.send(message).map_err(|_| light_errors::PEER_MISSING)
+    }
+
     async fn iterative_lookup(
         &self,
         dht_id: &DhtId,
@@ -234,33 +257,23 @@ impl KademliaDht {
                 AppRequestMessage::encode(&self.chain_id, sdk::FindValue { dht_id, bucket })
             {
                 let mail_tx = self.mail_tx.clone();
+                let chain_id = self.chain_id;
                 set.spawn(async move {
-                    let handle = sender.send_and_response(
-                        &mail_tx,
-                        SubscribableMessage::AppRequest(app_request),
-                    )?;
-                    let message = handle.await?;
-                    Result::<p2p::message::Message, NodeError>::Ok(message)
+                    sender
+                        .send_and_app_response(
+                            chain_id,
+                            constants::SNOWFLAKE_HANDLER_ID,
+                            &mail_tx,
+                            SubscribableMessage::AppRequest(app_request),
+                        )
+                        .await
                 });
             }
         }
 
         let mut nodes = HashSet::new();
         while let Some(result) = set.join_next().await {
-            let Ok(Ok(p2p::message::Message::AppResponse(app_response))) = result else {
-                continue;
-            };
-            if app_response.chain_id != self.chain_id.as_ref().to_vec() {
-                continue;
-            }
-            let bytes = app_response.app_bytes;
-            let Ok((app_id, bytes)) = unsigned_varint::decode::u64(&bytes) else {
-                continue;
-            };
-            if app_id != SNOWFLAKE_HANDLER_ID {
-                continue;
-            }
-            let Ok(light_message) = InboundMessage::decode(bytes) else {
+            let Ok(Ok(light_message)) = result else {
                 continue;
             };
             match light_message {
diff --git a/node/src/dht/mod.rs b/node/src/dht/mod.rs
index 5475979..ea4394e 100644
--- a/node/src/dht/mod.rs
+++ b/node/src/dht/mod.rs
@@ -69,7 +69,8 @@ impl<DB: LockedMapDb<Bucket, Vec<u8>>> Dht<DB> {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Deserialize, Copy, Clone)]
+#[serde(rename_all = "snake_case")]
 pub enum DhtId {
     Block,
     State,
@@ -95,6 +96,12 @@ impl From<&DhtId> for u32 {
     }
 }
 
+impl From<DhtId> for u32 {
+    fn from(value: DhtId) -> Self {
+        (&value).into()
+    }
+}
+
 #[derive(Debug)]
 pub enum LightMessage {
     NewPeer(DhtBuckets),
@@ -117,22 +124,34 @@ pub mod light_errors {
         code: 1,
         message: "Content not found",
     };
-    pub(crate) const DECODING_FAILED: LightError = LightError {
+    pub(crate) const ENCODING_FAILED: LightError = LightError {
         code: 2,
         message: "Invalid encoded value",
     };
-    pub(crate) const UNDESIRED_BUCKET: LightError = LightError {
+    pub(crate) const DECODING_FAILED: LightError = LightError {
         code: 3,
+        message: "Invalid encoded value",
+    };
+    pub(crate) const UNDESIRED_BUCKET: LightError = LightError {
+        code: 4,
         message: "This bucket is not desired",
     };
     pub(crate) const INVALID_DHT: LightError = LightError {
-        code: 4,
+        code: 5,
         message: "Unimplemented dht",
     };
     pub(crate) const INVALID_CONTENT: LightError = LightError {
-        code: 5,
+        code: 6,
         message: "Content failed verification",
     };
+    pub(crate) const PEER_MISSING: LightError = LightError {
+        code: 7,
+        message: "The peer is missing",
+    };
+    pub(crate) const SEND_TO_SELF: LightError = LightError {
+        code: 8,
+        message: "Cannot send message to self",
+    };
 }
 
 impl From<LightError> for jsonrpsee::types::ErrorObjectOwned {
diff --git a/node/src/id/block.rs b/node/src/id/block.rs
index c2445fc..eae54d3 100644
--- a/node/src/id/block.rs
+++ b/node/src/id/block.rs
@@ -1,6 +1,6 @@
 use super::{Id, IdError};
 
-#[derive(Debug, Clone, Eq, Hash, PartialEq, Default)]
+#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Default)]
 pub struct BlockID {
     id: Id<{ Self::LEN }>,
 }
@@ -26,6 +26,16 @@ impl TryFrom<&[u8]> for BlockID {
     }
 }
 
+impl TryFrom<&str> for BlockID {
+    type Error = IdError;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        Ok(Self {
+            id: Id::try_from(value)?,
+        })
+    }
+}
+
 impl std::fmt::Display for BlockID {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", self.id)
diff --git a/node/src/id/chain.rs b/node/src/id/chain.rs
index 6707c4b..64030de 100644
--- a/node/src/id/chain.rs
+++ b/node/src/id/chain.rs
@@ -3,7 +3,7 @@ use super::Id;
 #[allow(unused)]
 pub const CHAIN_ID_BYTES_LEN: usize = 32;
 
-#[derive(PartialEq, Debug, Clone)]
+#[derive(PartialEq, Debug, Clone, Copy)]
 pub struct ChainId(pub(crate) Id<{ Self::LEN }>);
 
 impl ChainId {
diff --git a/node/src/id/mod.rs b/node/src/id/mod.rs
index 0cd8f23..de9ae18 100644
--- a/node/src/id/mod.rs
+++ b/node/src/id/mod.rs
@@ -5,7 +5,9 @@ pub use chain::*;
 mod node;
 pub use node::*;
 
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use std::cmp::Ordering;
+use std::fmt::Formatter;
 use thiserror::Error;
 
 #[derive(Debug, Error)]
@@ -18,7 +20,7 @@ pub enum IdError {
     CB58DecodeError,
 }
 
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Clone, Copy, Hash, PartialEq, Eq)]
 pub struct Id<const LEN: usize> {
     inner: [u8; LEN],
 }
@@ -76,6 +78,12 @@ impl<const LEN: usize> std::fmt::Display for Id<LEN> {
     }
 }
 
+impl<const LEN: usize> std::fmt::Debug for Id<LEN> {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self)
+    }
+}
+
 impl<const LEN: usize> PartialOrd for Id<LEN> {
     fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
         Some(self.cmp(other))
@@ -94,3 +102,23 @@ impl<const LEN: usize> Ord for Id<LEN> {
         Ordering::Equal
     }
 }
+
+impl<const LEN: usize> Serialize for Id<LEN> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
+impl<'de, const LEN: usize> Deserialize<'de> for Id<LEN> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let id_string = <String>::deserialize(deserializer)?;
+        let id = Id::try_from(id_string.as_str()).map_err(serde::de::Error::custom)?;
+        Ok(id)
+    }
+}
diff --git a/node/src/id/node.rs b/node/src/id/node.rs
index 3b111a8..2d7f769 100644
--- a/node/src/id/node.rs
+++ b/node/src/id/node.rs
@@ -1,5 +1,6 @@
 use super::{Id, IdError};
 use ripemd::Ripemd160;
+use serde::{Deserialize, Deserializer, Serialize};
 use sha2::{Digest, Sha256};
 use std::fmt::{Debug, Display, Formatter};
 
@@ -100,6 +101,27 @@ impl TryFrom<&str> for NodeId {
     }
 }
 
+impl<'de> Deserialize<'de> for NodeId {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let node_id_string = <String>::deserialize(deserializer)?;
+        let node_id =
+            NodeId::try_from(node_id_string.as_str()).map_err(serde::de::Error::custom)?;
+        Ok(node_id)
+    }
+}
+
+impl Serialize for NodeId {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(&self.to_string())
+    }
+}
+
 impl NodeId {
     pub const LEN: usize = 20;
 
diff --git a/node/src/net/light.rs b/node/src/net/light.rs
index 7a99a14..86b5fcc 100644
--- a/node/src/net/light.rs
+++ b/node/src/net/light.rs
@@ -10,12 +10,12 @@ use crate::net::RwLock;
 use crate::net::{LightError, Network};
 use crate::node::Node;
 use crate::server::peers::PeerInfo;
-use crate::utils::rlp::Block;
-use crate::utils::unpacker::StatelessBlock;
 use crate::Arc;
 use flume::Sender;
 use indexmap::IndexMap;
-use std::collections::{HashMap, HashSet};
+#[cfg(test)]
+use std::collections::HashMap;
+use std::collections::HashSet;
 use std::sync::{LockResult, RwLockReadGuard, RwLockWriteGuard};
 use tokio::sync::broadcast;
 use tokio::sync::oneshot;
@@ -45,8 +45,7 @@ impl LightNetwork {
         config: LightNetworkConfig,
         max_light_peers: Option<usize>,
     ) -> Self {
-        let store = RwLock::new(HashMap::new());
-        let block_dht = Arc::new(DhtBlocks::new(node_id, Bucket::from(10), store));
+        let block_dht = Arc::new(DhtBlocks::new(node_id));
         let light_peers = LightPeers::new(
             node_id,
             peers_infos.clone(),
@@ -80,7 +79,7 @@ impl LightNetwork {
         Ok(())
     }
 
-    pub fn manage_message(
+    pub async fn manage_message(
         &self,
         node_id: &NodeId,
         message: LightMessage,
@@ -96,6 +95,7 @@ impl LightNetwork {
                     DhtId::Block => self
                         .block_dht
                         .insert_to_store(value)
+                        .await
                         .map(|_| LightValue::Ok),
                     _ => Err(light_errors::INVALID_DHT),
                 };
@@ -103,13 +103,13 @@ impl LightNetwork {
             }
             LightMessage::FindNode(bucket) => {
                 let res = Ok(LightValue::ValueOrNodes(ValueOrNodes::Nodes(
-                    self.find_node(&bucket),
+                    self.kademlia_dht.find_node(&bucket),
                 )));
                 Some(res)
             }
             LightMessage::FindValue(dht_id, bucket) => {
                 let res = match dht_id {
-                    DhtId::Block => self.find_value(&self.block_dht.store, &bucket),
+                    DhtId::Block => self.find_value(&self.block_dht.dht.store, &bucket),
                     _ => Err(light_errors::INVALID_DHT),
                 };
                 Some(res)
@@ -133,11 +133,6 @@ impl LightNetwork {
         }
     }
 
-    /// Lookup locally for nodes spanning the bucket.
-    fn find_node(&self, bucket: &Bucket) -> Vec<ConnectionData> {
-        self.kademlia_dht.find_node(bucket)
-    }
-
     /// Lookup locally for a value or nodes spanning the bucket.
     fn find_value<DB>(&self, db: &DB, bucket: &Bucket) -> LightResult
     where
@@ -145,14 +140,14 @@ impl LightNetwork {
     {
         let value_or_nodes = match db.get(bucket) {
             Some(value) => ValueOrNodes::Value(value),
-            None => ValueOrNodes::Nodes(self.find_node(bucket)),
+            None => ValueOrNodes::Nodes(self.kademlia_dht.find_node(bucket)),
         };
         Ok(LightValue::ValueOrNodes(value_or_nodes))
     }
 
     /// Check if the value is stored locally.
     /// If not, lookup the DHT for it.
-    async fn find_content<DHT, K, V>(&self, dht: &Arc<DHT>, key: K) -> Result<V, LightError>
+    pub async fn find_content<DHT, K, V>(&self, dht: &Arc<DHT>, key: K) -> Result<V, LightError>
     where
         DHT: ConcreteDht<K> + DhtContent<K, V>,
     {
@@ -177,10 +172,22 @@ impl LightNetwork {
         }
     }
 
-    pub async fn find_block(&self, number: u64) -> Result<Block, LightError> {
-        self.find_content(&self.block_dht, number)
-            .await
-            .map(|stateless_block| stateless_block.block)
+    pub async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded).await?;
+            Ok(())
+        } else {
+            self.kademlia_dht.store(node_id, &DHT::id(), encoded).await
+        }
     }
 }
 
@@ -189,60 +196,17 @@ pub trait DhtContent<K, V>: ConcreteDht<K> {
     fn get_from_store(&self, bucket: &Bucket) -> Result<Option<V>, LightError>;
     /// Insert an encoded value into the store.
     /// If the value is ill-formed, it should not be stored.
-    fn insert_to_store(&self, bytes: Vec<u8>) -> Result<Option<V>, LightError>;
+    async fn insert_to_store(&self, bytes: Vec<u8>) -> Result<Option<V>, LightError>;
     /// Verification of the validity of the content.
     /// It is used to check if the content is well-formed.
     /// If the content is ill-formed, it should not be stored.
-    fn verify(&self, value: &V) -> bool;
+    async fn verify(&self, value: &V) -> Result<bool, LightError>;
     /// Typed to encoded value
-    #[allow(unused)]
     fn encode(value: V) -> Result<Vec<u8>, LightError>;
     /// Encoded to typed value
     fn decode(bytes: &[u8]) -> Result<V, LightError>;
 }
 
-impl DhtContent<u64, StatelessBlock> for DhtBlocks {
-    fn get_from_store(&self, bucket: &Bucket) -> Result<Option<StatelessBlock>, LightError> {
-        match self.store.get(bucket) {
-            Some(block_bytes) => {
-                let block = Self::decode(&block_bytes)?;
-                Ok(Some(block))
-            }
-            None => Ok(None),
-        }
-    }
-
-    fn insert_to_store(&self, bytes: Vec<u8>) -> Result<Option<StatelessBlock>, LightError> {
-        let decoded = Self::decode(&bytes)?;
-        if !self.verify(&decoded) {
-            return Err(light_errors::INVALID_CONTENT);
-        }
-        let number = u64::from_be_bytes(*decoded.block.header.number());
-        let bucket = Self::key_to_bucket(number);
-        if !self.is_desired_bucket(&bucket) {
-            return Err(light_errors::UNDESIRED_BUCKET);
-        }
-        match self.store.insert(bucket, bytes) {
-            Some(ret_bytes) => Ok(Some(Self::decode(&ret_bytes)?)),
-            None => Ok(None),
-        }
-    }
-
-    fn verify(&self, _block: &StatelessBlock) -> bool {
-        // TODO implement robust block verification i.e make sure that the hash matches at least.
-        //   Then, send GetAccepted message to bootstrap node if this block doesn't come from a bootstrap node.
-        true
-    }
-
-    fn encode(_value: StatelessBlock) -> Result<Vec<u8>, LightError> {
-        todo!()
-    }
-
-    fn decode(bytes: &[u8]) -> Result<StatelessBlock, LightError> {
-        StatelessBlock::unpack(bytes).map_err(|_| light_errors::DECODING_FAILED)
-    }
-}
-
 #[derive(Debug, Clone)]
 pub struct LightPeers {
     node_id: NodeId,
@@ -443,7 +407,7 @@ mod tests {
         let far_peer = NodeId::from([0xF0; 20]);
         let medium_peer = NodeId::from([0x80; 20]);
 
-        let peers = vec![far_peer, medium_peer];
+        let peers = [far_peer, medium_peer];
 
         let peers_light: HashMap<_, _> = peers
             .iter()
diff --git a/node/src/net/mod.rs b/node/src/net/mod.rs
index 193c6cb..4b82269 100644
--- a/node/src/net/mod.rs
+++ b/node/src/net/mod.rs
@@ -1,11 +1,12 @@
 use crate::dht::kademlia::ValueOrNodes;
 use crate::dht::LightError;
-use crate::dht::{Bucket, DhtId, LightMessage, LightResult};
+use crate::dht::{Bucket, LightMessage, LightResult};
 use crate::dht::{DhtBuckets, LightValue};
 use crate::id::{ChainId, NodeId};
 use crate::message::mail_box::Mail;
 use crate::message::{mail_box::MailBox, pipeline::Pipeline, MiniMessage, SubscribableMessage};
 use crate::net::node::SendErrorWrapper;
+use crate::net::sdk::Store;
 use crate::net::sdk::{FindNode, FindValue, LightHandshake};
 use crate::net::{
     ip::SignedIp,
@@ -630,6 +631,14 @@ impl Peer {
         log::trace!("received light message {light_message:?}");
         let chain_id = c_chain_id.as_ref().to_vec();
         let res = match light_message {
+            sdk::light_request::Message::Store(Store { dht_id, value }) => {
+                let dht_id = dht_id
+                    .try_into()
+                    .map_err(|_| NodeError::Message("unsupported DHT".to_string()))?;
+                spl.send((LightMessage::Store(dht_id, value), None))
+                    .map_err(SendErrorWrapper::from)?;
+                None
+            }
             sdk::light_request::Message::LightHandshake(LightHandshake { buckets }) => {
                 if let Some(buckets) = buckets {
                     let bucket_arr: [u8; 20] = buckets
@@ -644,10 +653,9 @@ impl Peer {
                 }
             }
             sdk::light_request::Message::FindValue(FindValue { dht_id, bucket }) => {
-                let dht_id = match dht_id {
-                    0 => DhtId::Block,
-                    _ => return Err(NodeError::Message("unsupported DHT".to_string())),
-                };
+                let dht_id = dht_id
+                    .try_into()
+                    .map_err(|_| NodeError::Message("unsupported DHT".to_string()))?;
                 let bucket_arr: [u8; 20] = bucket
                     .try_into()
                     .map_err(|_| NodeError::Message("invalid bucket".to_string()))?;
diff --git a/node/src/net/node.rs b/node/src/net/node.rs
index f3ec3d7..4417561 100644
--- a/node/src/net/node.rs
+++ b/node/src/net/node.rs
@@ -3,6 +3,7 @@ use crate::dht::DhtBuckets;
 use crate::id::{ChainId, NodeId};
 use crate::message::{pipeline::Pipeline, MiniMessage};
 use crate::net::{ip::UnsignedIp, BackoffParams, Intervals, Network, PeerInfo};
+use crate::node::SinglePickerConfig;
 use crate::server::msg::AppRequestMessage;
 use crate::server::{
     msg::{DecodingError, OutboundMessage},
@@ -21,7 +22,7 @@ use indexmap::IndexMap;
 use prost::EncodeError;
 use proto_lib::p2p::{self};
 use proto_lib::sdk;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::net::SocketAddr;
 use std::path::PathBuf;
 use std::sync::RwLockWriteGuard;
@@ -442,4 +443,36 @@ impl Network {
         }
         Ok(())
     }
+
+    pub fn pick_peer(
+        peers_infos: &Arc<RwLock<IndexMap<NodeId, PeerInfo>>>,
+        bootstrappers: &RwLock<HashMap<NodeId, Option<DhtBuckets>>>,
+        config: SinglePickerConfig,
+    ) -> Option<NodeId> {
+        match config {
+            SinglePickerConfig::Bootstrapper => {
+                let peers = peers_infos.read().unwrap();
+                let available_peers: HashSet<_> = peers.keys().collect();
+                let bootstrappers = bootstrappers.read().unwrap();
+                let bootstrappers: HashSet<_> = bootstrappers
+                    .iter()
+                    .filter_map(|(node_id, buckets)| {
+                        if buckets.is_none() {
+                            Some(node_id)
+                        } else {
+                            None
+                        }
+                    })
+                    .collect();
+                let inter: Vec<_> = bootstrappers.intersection(&available_peers).collect();
+                if inter.is_empty() {
+                    None
+                } else {
+                    let i = (rand::random::<u64>() % inter.len() as u64) as usize;
+                    Some(**inter[i])
+                }
+            }
+            _ => todo!(),
+        }
+    }
 }
diff --git a/node/src/node.rs b/node/src/node.rs
index c6ccbf8..dd856b2 100644
--- a/node/src/node.rs
+++ b/node/src/node.rs
@@ -35,6 +35,7 @@ use tokio::sync::{broadcast, OwnedSemaphorePermit, Semaphore};
 use tokio::task::JoinHandle;
 use tokio::time::{self};
 
+#[derive(Debug)]
 pub struct Node {
     pub(crate) network: Arc<Network>,
     pub(crate) light_network: LightNetwork,
@@ -76,14 +77,14 @@ impl Node {
 
         let connection_queue = Arc::new(ConnectionQueue::new(max_concurrent));
         let peers_infos = Arc::new(RwLock::new(IndexMap::new()));
-        let network = Network::new(network_config, node_id, peers_infos.clone()).unwrap();
+        let network = Arc::new(Network::new(network_config, node_id, peers_infos.clone()).unwrap());
         let mail_box = MailBox::new(max_latency_records);
         let light_network = LightNetwork::new(
             network.node_id,
             peers_infos,
             connection_queue.clone(),
             mail_box.tx().clone(),
-            network.config.c_chain_id.clone(),
+            network.config.c_chain_id,
             LightNetworkConfig {
                 sync_headers,
                 max_lookups: 10,
@@ -93,7 +94,7 @@ impl Node {
         );
 
         Self {
-            network: Arc::new(network),
+            network,
             light_network,
             connection_queue,
             mail_box: Arc::new(mail_box),
@@ -147,6 +148,7 @@ impl Node {
         let node = self.clone();
         let rx2 = rx.resubscribe();
         // TODO this is probably bad design to have the node being passed twice here.
+        // TODO The light network may be independent.
         let light = tokio::spawn(async move { node.light_network.start(node.clone(), rx2).await });
 
         let node = self.clone();
@@ -282,7 +284,7 @@ impl Node {
         NodeError,
     > {
         let node_id = *peer.node_id();
-        let c_chain_id = self.network.config.c_chain_id.clone();
+        let c_chain_id = self.network.config.c_chain_id;
 
         let sender = peer.sender().clone();
         let (tx, _) = broadcast::channel(100);
@@ -354,6 +356,9 @@ impl Node {
     ) {
         let mut maybe_hs_permit = Some(hs_permit);
 
+        // TODO: this is single-threaded and the verification in the light_network.manage_message
+        //  function is asynchronous and may take some time, which will bloat message processing.
+        //  We should make it multi-threaded.
         loop {
             log::trace!("execute");
             tokio::select! {
@@ -364,7 +369,7 @@ impl Node {
                 }
                 res = rpl.recv_async() => {
                     if let Ok((msg, resp)) = res {
-                        self.light_network.manage_message(&node_id, msg, resp);
+                        self.light_network.manage_message(&node_id, msg, resp).await;
                     }
                 }
                 _ = rx.recv() => {
@@ -674,34 +679,6 @@ impl Node {
         peers.into_iter().collect()
     }
 
-    pub fn pick_peer(&self, config: SinglePickerConfig) -> Option<NodeId> {
-        match config {
-            SinglePickerConfig::Bootstrapper => {
-                let peers = self.network.peers_infos.read().unwrap();
-                let available_peers: HashSet<_> = peers.keys().collect();
-                let bootstrappers = self.network.bootstrappers.read().unwrap();
-                let bootstrappers: HashSet<_> = bootstrappers
-                    .iter()
-                    .filter_map(|(node_id, buckets)| {
-                        if buckets.is_none() {
-                            Some(node_id)
-                        } else {
-                            None
-                        }
-                    })
-                    .collect();
-                let inter: Vec<_> = bootstrappers.intersection(&available_peers).collect();
-                if inter.is_empty() {
-                    None
-                } else {
-                    let i = (rand::random::<u64>() % inter.len() as u64) as usize;
-                    Some(**inter[i])
-                }
-            }
-            _ => todo!(),
-        }
-    }
-
     pub async fn send_to_peers(
         &self,
         node_ids: &[NodeId],
diff --git a/node/src/server/msg.rs b/node/src/server/msg.rs
index 3fd485e..9bfa72f 100644
--- a/node/src/server/msg.rs
+++ b/node/src/server/msg.rs
@@ -42,11 +42,11 @@ pub enum DecodingError {
     EmptyMessage,
 }
 
-pub trait InboundMessageExt<OUTER, MESSAGE> {
+pub trait InboundMessageExt<MESSAGE> {
     fn decode(message: &[u8]) -> Result<MESSAGE, DecodingError>;
 }
 
-impl InboundMessageExt<p2p::Message, p2p::message::Message> for InboundMessage {
+impl InboundMessageExt<p2p::message::Message> for InboundMessage {
     fn decode(message: &[u8]) -> Result<p2p::message::Message, DecodingError> {
         let len = message.len();
         if len == 0 {
@@ -59,7 +59,7 @@ impl InboundMessageExt<p2p::Message, p2p::message::Message> for InboundMessage {
     }
 }
 
-impl InboundMessageExt<sdk::LightRequest, sdk::light_request::Message> for InboundMessage {
+impl InboundMessageExt<sdk::light_request::Message> for InboundMessage {
     fn decode(message: &[u8]) -> Result<sdk::light_request::Message, DecodingError> {
         let len = message.len();
         if len == 0 {
@@ -72,7 +72,7 @@ impl InboundMessageExt<sdk::LightRequest, sdk::light_request::Message> for Inbou
     }
 }
 
-impl InboundMessageExt<sdk::LightResponse, sdk::light_response::Message> for InboundMessage {
+impl InboundMessageExt<sdk::light_response::Message> for InboundMessage {
     fn decode(message: &[u8]) -> Result<sdk::light_response::Message, DecodingError> {
         let len = message.len();
         if len == 0 {
diff --git a/node/src/server/peers.rs b/node/src/server/peers.rs
index 3ea78fd..0839be8 100644
--- a/node/src/server/peers.rs
+++ b/node/src/server/peers.rs
@@ -1,4 +1,6 @@
+use crate::id::ChainId;
 use crate::net::node::SendErrorWrapper;
+use crate::server::msg::{InboundMessage, InboundMessageExt};
 use crate::{
     id::NodeId,
     message::{mail_box::Mail, SubscribableMessage},
@@ -69,9 +71,6 @@ impl PeerSender {
         self.send(message.into())
     }
 
-    // TODO: Instead, provide a generic that automatically resolves to the correct message
-    //  enum variant with specific impls. Also, it seems like we could just return the message
-    //  instead of the oneshot channel since it's always awaited after calling the function.
     pub fn send_and_response(
         &self,
         mail_tx: &Sender<Mail>,
@@ -82,6 +81,35 @@ impl PeerSender {
         Ok(rx)
     }
 
+    pub async fn send_and_app_response<MESSAGE>(
+        &self,
+        chain_id: ChainId,
+        handler_id: u64,
+        mail_tx: &Sender<Mail>,
+        message: SubscribableMessage,
+    ) -> Result<MESSAGE, NodeError>
+    where
+        InboundMessage: InboundMessageExt<MESSAGE>,
+    {
+        let rx = self.send_and_response(mail_tx, message)?;
+        let Message::AppResponse(app_response) = rx.await? else {
+            return Err(NodeError::Message(
+                "received an invalid message".to_string(),
+            ));
+        };
+        if app_response.chain_id != chain_id.as_ref().to_vec() {
+            return Err(NodeError::Message("invalid chain ID".to_string()));
+        }
+        let bytes = app_response.app_bytes;
+        let Ok((app_id, bytes)) = unsigned_varint::decode::u64(&bytes) else {
+            return Err(NodeError::Message("invalid uvarint encoding".to_string()));
+        };
+        if app_id != handler_id {
+            return Err(NodeError::Message("invalid handler ID".to_string()));
+        }
+        Ok(InboundMessage::decode(bytes)?)
+    }
+
     pub fn send_without_response(
         &self,
         mail_tx: &Sender<Mail>,
diff --git a/node/src/server/rpc.rs b/node/src/server/rpc.rs
index 0a931cb..cc439c5 100644
--- a/node/src/server/rpc.rs
+++ b/node/src/server/rpc.rs
@@ -33,7 +33,18 @@ macro_rules! not_implemented {
 
 mod rpc_impl {
     use super::*;
+    use crate::dht::block::DhtBlocks;
+    use crate::dht::light_errors;
+    use crate::dht::Bucket;
+    use crate::dht::DhtId;
+    use crate::id::NodeId;
+    use crate::message::SubscribableMessage;
+    use crate::net::light::DhtContent;
+    use crate::net::queue::ConnectionData;
     use crate::node::Node;
+    use crate::server::msg::AppRequestMessage;
+    use crate::server::msg::InboundMessage;
+    use crate::server::msg::InboundMessageExt;
     use crate::utils::constants;
     use crate::utils::rlp::{Block, Transaction};
     use crate::Arc;
@@ -44,6 +55,7 @@ mod rpc_impl {
     use jsonrpsee::core::{async_trait, RpcResult};
     use jsonrpsee::proc_macros::rpc;
     use jsonrpsee::types::ErrorObject;
+    use proto_lib::{p2p, sdk};
     use serde::{Deserialize, Deserializer, Serialize};
     use std::env;
     use std::str::FromStr;
@@ -368,6 +380,46 @@ mod rpc_impl {
         fn get_logs(&self, filter_object: FilterObject) -> RpcResult<Vec<LogObject>>;
     }
 
+    #[derive(Serialize, Clone)]
+    pub enum Value {
+        Block(alloy::rpc::types::Block),
+    }
+
+    #[derive(Serialize, Clone)]
+    pub enum RpcValueOrNodes {
+        Value(Value),
+        Nodes(Vec<NodeId>),
+    }
+
+    #[rpc(server, namespace = "light")]
+    pub trait Light {
+        #[method(name = "ping")]
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()>;
+
+        #[method(name = "store")]
+        async fn store(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            value: Bytes,
+        ) -> RpcResult<()>;
+
+        #[method(name = "find_node")]
+        async fn find_node(
+            &self,
+            node_id: Option<NodeId>,
+            bucket: Bucket,
+        ) -> RpcResult<Vec<NodeId>>;
+
+        #[method(name = "find_value")]
+        async fn find_value(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            bucket: Bucket,
+        ) -> RpcResult<RpcValueOrNodes>;
+    }
+
     #[derive(Clone)]
     pub struct RpcServerImpl {
         pub(crate) node: Arc<Node>,
@@ -447,8 +499,12 @@ mod rpc_impl {
                 BlockParameter::Number(number) => number,
                 _ => not_implemented!(), // resolve block
             };
-            let block = self.node.light_network.find_block(number).await?;
-            Ok(block.transactions.len() as u64)
+            let block = self
+                .node
+                .light_network
+                .find_content(&self.node.light_network.block_dht, number)
+                .await?;
+            Ok(block.block.transactions.len() as u64)
         }
 
         fn get_uncle_count_by_block_hash(
@@ -534,9 +590,9 @@ mod rpc_impl {
             };
             self.node
                 .light_network
-                .find_block(number)
+                .find_content(&self.node.light_network.block_dht, number)
                 .await
-                .map(|block| block_to_rpc(block, full))
+                .map(|block| block_to_rpc(block.block, full))
                 .map_err(Into::into)
         }
 
@@ -611,11 +667,238 @@ mod rpc_impl {
             not_implemented!()
         }
     }
+
+    #[async_trait]
+    impl LightServer for RpcServerImpl {
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()> {
+            if node_id == self.node.network.node_id {
+                return Err(light_errors::SEND_TO_SELF.into());
+            }
+            let maybe_peer_infos = {
+                let peers_infos = self
+                    .node
+                    .light_network
+                    .kademlia_dht
+                    .peers_infos
+                    .read()
+                    .unwrap();
+                peers_infos.get(&node_id).cloned()
+            };
+            if let Some(peer_infos) = maybe_peer_infos {
+                peer_infos
+                    .ping(&self.node.light_network.kademlia_dht.mail_tx)
+                    .await
+                    .map_err(|err| ErrorObject::owned(1000, err.to_string(), None::<()>))
+            } else {
+                Err(light_errors::PEER_MISSING.into())
+            }
+        }
+
+        async fn store(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            value: Bytes,
+        ) -> RpcResult<()> {
+            let node_id = if node_id.is_none() {
+                self.node.network.node_id
+            } else {
+                node_id.unwrap()
+            };
+            match dht_id {
+                DhtId::Block => {
+                    let block = DhtBlocks::decode(&value)?;
+                    self.node
+                        .light_network
+                        .store(&self.node.light_network.block_dht, node_id, block)
+                        .await?;
+                }
+                DhtId::State => {}
+            }
+            Ok(())
+        }
+
+        async fn find_node(
+            &self,
+            node_id: Option<NodeId>,
+            bucket: Bucket,
+        ) -> RpcResult<Vec<NodeId>> {
+            let node_ids = if node_id.is_none() || node_id == Some(self.node.network.node_id) {
+                self.node
+                    .light_network
+                    .kademlia_dht
+                    .find_node(&bucket)
+                    .into_iter()
+                    .map(|ConnectionData { node_id, .. }| node_id)
+                    .collect()
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindNode {
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let light_message: sdk::light_response::Message = sender
+                    .send_and_app_response(
+                        self.node.light_network.kademlia_dht.chain_id,
+                        constants::SNOWFLAKE_HANDLER_ID,
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .await
+                    .map_err(|err| ErrorObject::owned(1000, err.to_string(), None::<()>))?;
+                match light_message {
+                    sdk::light_response::Message::Nodes(p2p::PeerList { claimed_ip_ports }) => {
+                        if claimed_ip_ports.len() > 10 {
+                            return Err(light_errors::INVALID_CONTENT.into());
+                        }
+                        claimed_ip_ports
+                            .into_iter()
+                            .filter_map(|claimed_ip_port| claimed_ip_port.try_into().ok())
+                            .map(|ConnectionData { node_id, .. }| node_id)
+                            .collect()
+                    }
+                    _ => return Err(light_errors::INVALID_CONTENT.into()),
+                }
+            };
+            Ok(node_ids)
+        }
+
+        async fn find_value(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            bucket: Bucket,
+        ) -> RpcResult<RpcValueOrNodes> {
+            let value_or_nodes = if node_id.is_none() || node_id == Some(self.node.network.node_id)
+            {
+                match dht_id {
+                    DhtId::Block => {
+                        let store = self.node.light_network.block_dht.dht.store.read().unwrap();
+                        match store.get(&bucket) {
+                            Some(value) => RpcValueOrNodes::Value(Value::Block(block_to_rpc(
+                                DhtBlocks::decode(value)?.block,
+                                true,
+                            ))),
+                            None => {
+                                let connections_data =
+                                    self.node.light_network.kademlia_dht.find_node(&bucket);
+                                let node_ids = connections_data
+                                    .into_iter()
+                                    .map(|ConnectionData { node_id, .. }| node_id)
+                                    .collect();
+                                RpcValueOrNodes::Nodes(node_ids)
+                            }
+                        }
+                    }
+                    _ => return Err(light_errors::INVALID_DHT.into()),
+                }
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindValue {
+                        dht_id: dht_id.into(),
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let rx = sender
+                    .send_and_response(
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .map_err(|err| ErrorObject::owned(1000, err.to_string(), None::<()>))?;
+
+                let p2p::message::Message::AppResponse(app_response) = rx
+                    .await
+                    .map_err(|_| ErrorObject::borrowed(1000, "timeout", None))?
+                else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                if app_response.chain_id
+                    != self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .chain_id
+                        .as_ref()
+                        .to_vec()
+                {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                }
+                let bytes = app_response.app_bytes;
+                let Ok((app_id, bytes)) = unsigned_varint::decode::u64(&bytes) else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                if app_id != constants::SNOWFLAKE_HANDLER_ID {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                }
+                let Ok(light_message) = InboundMessage::decode(bytes) else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                match light_message {
+                    sdk::light_response::Message::Value(sdk::Value { value }) => match dht_id {
+                        DhtId::Block => {
+                            let block = DhtBlocks::decode(&value)?;
+                            RpcValueOrNodes::Value(Value::Block(block_to_rpc(block.block, true)))
+                        }
+                        _ => return Err(light_errors::INVALID_DHT.into()),
+                    },
+                    sdk::light_response::Message::Nodes(p2p::PeerList { claimed_ip_ports }) => {
+                        if claimed_ip_ports.len() > 10 {
+                            return Err(light_errors::INVALID_CONTENT.into());
+                        }
+                        let node_ids = claimed_ip_ports
+                            .into_iter()
+                            .filter_map(|claimed_ip_port| claimed_ip_port.try_into().ok())
+                            .map(|ConnectionData { node_id, .. }| node_id)
+                            .collect();
+                        RpcValueOrNodes::Nodes(node_ids)
+                    }
+                    _ => return Err(light_errors::INVALID_CONTENT.into()),
+                }
+            };
+            Ok(value_or_nodes)
+        }
+    }
 }
 
 use crate::net::node::NodeError;
 use crate::node::Node;
-use rpc_impl::{EthServer, NetServer, RpcServerImpl, Web3Server};
+use rpc_impl::{EthServer, LightServer, NetServer, RpcServerImpl, Web3Server};
 
 impl Rpc {
     pub async fn new(
@@ -649,7 +932,9 @@ impl Rpc {
         let mut rpc = Web3Server::into_rpc(rpc_impl.clone());
         rpc.merge(NetServer::into_rpc(rpc_impl.clone()))
             .expect("should not fail");
-        rpc.merge(EthServer::into_rpc(rpc_impl))
+        rpc.merge(EthServer::into_rpc(rpc_impl.clone()))
+            .expect("should not fail");
+        rpc.merge(LightServer::into_rpc(rpc_impl))
             .expect("should not fail");
 
         let server_handle = self.server.start(rpc);
diff --git a/node/src/utils/fifo_set.rs b/node/src/utils/fifo_set.rs
index c884d14..ba0df74 100644
--- a/node/src/utils/fifo_set.rs
+++ b/node/src/utils/fifo_set.rs
@@ -3,6 +3,7 @@
 use crate::utils::FIFO;
 use std::{collections::HashSet, hash::Hash};
 
+#[derive(Debug)]
 pub struct FIFOSet<T> {
     set: HashSet<T>,
     cache: FIFO<T>,
@@ -19,6 +20,8 @@ where
         }
     }
 
+    // TODO adapt this method to only insert in the cache if the set doesn't contain this value.
+    //  If it already contains it, then we should make this value the last one.
     pub fn insert(&mut self, element: T) -> Option<T> {
         let res = self.cache.push(element.clone());
 
diff --git a/node/src/utils/mod.rs b/node/src/utils/mod.rs
index 1e913e5..316294f 100644
--- a/node/src/utils/mod.rs
+++ b/node/src/utils/mod.rs
@@ -10,3 +10,4 @@ pub mod unpacker;
 pub mod windower;
 
 pub use fifo::FIFO;
+pub use fifo_set::FIFOSet;
diff --git a/node/src/utils/packer.rs b/node/src/utils/packer.rs
index d086f92..7505249 100644
--- a/node/src/utils/packer.rs
+++ b/node/src/utils/packer.rs
@@ -6,6 +6,10 @@ pub struct Packer {
 }
 
 impl Packer {
+    pub fn new() -> Self {
+        Self { bytes: Vec::new() }
+    }
+
     pub fn new_with_capacity(capacity: usize) -> Self {
         Self {
             bytes: Vec::with_capacity(capacity),
@@ -16,12 +20,21 @@ impl Packer {
         self.bytes.extend_from_slice(bytes)
     }
 
+    pub fn pack_bytes(&mut self, bytes: &[u8]) {
+        self.pack_med(&(bytes.len() as u32));
+        self.bytes.extend_from_slice(bytes);
+    }
+
     pub fn pack_short(&mut self, short: &u16) {
-        self.bytes.append(&mut short.to_be_bytes().to_vec())
+        self.bytes.extend_from_slice(&short.to_be_bytes())
+    }
+
+    pub fn pack_med(&mut self, med: &u32) {
+        self.bytes.extend_from_slice(&med.to_be_bytes())
     }
 
     pub fn pack_long(&mut self, long: &u64) {
-        self.bytes.append(&mut long.to_be_bytes().to_vec())
+        self.bytes.extend_from_slice(&long.to_be_bytes())
     }
 
     pub fn finish(self) -> Vec<u8> {
diff --git a/node/src/utils/rlp.rs b/node/src/utils/rlp.rs
index 011eef7..e853057 100644
--- a/node/src/utils/rlp.rs
+++ b/node/src/utils/rlp.rs
@@ -26,17 +26,19 @@ pub struct Block {
     pub transactions: Vec<Transaction>,
     #[allow(unused)]
     pub uncles: Vec<Header>,
+    pub version: u32,
+    pub ext_data: Vec<u8>,
 }
 
 impl Block {
     pub fn decode(bytes: &[u8]) -> Result<Self, RlpError> {
         let mut cursor = 0;
-        // let start_list = cursor;
-        let _length = Rlp::decode_list(bytes, &mut cursor)?;
+        let length = Rlp::decode_list(bytes, &mut cursor)?;
+        let start_list = cursor;
         let (header, hash) = {
-            let start_list = cursor;
+            let start_outer_list = cursor;
             let length = Rlp::decode_list(bytes, &mut cursor)?;
-            let hash = keccak256(&bytes[start_list..cursor + length as usize]);
+            let hash = keccak256(&bytes[start_outer_list..cursor + length as usize]);
             let header = Self::decode_header(&bytes[cursor..cursor + length as usize])?;
             cursor += length as usize;
             (header, hash)
@@ -44,22 +46,42 @@ impl Block {
         let transactions = {
             let length = Rlp::decode_list(bytes, &mut cursor)?;
             let transactions = Self::decode_transactions(&bytes[cursor..cursor + length as usize])?;
-            // cursor += length as usize;
+            cursor += length as usize;
             #[allow(clippy::let_and_return)]
             transactions
         };
-        // TODO decode uncles
-        // dbg!(&bytes[cursor..]);
-        // if cursor != start_list + length as usize {
-        //     return Err(RlpError::InvalidFormat);
-        // }
+        let uncles = {
+            let length = Rlp::decode_list(bytes, &mut cursor)?;
+            let start_list = cursor;
+            let mut uncles = Vec::new();
+            while cursor < start_list + length as usize {
+                let length = Rlp::decode_list(bytes, &mut cursor)?;
+                let header = Self::decode_header(&bytes[cursor..cursor + length as usize])?;
+                cursor += length as usize;
+                uncles.push(header);
+            }
+            if cursor != start_list + length as usize {
+                return Err(RlpError::InvalidFormat);
+            }
+            uncles
+        };
+        let version = {
+            let arr = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            u32::from_be_bytes(arr)
+        };
+        let ext_data = Rlp::decode_string(bytes, &mut cursor)?.to_vec();
+        if cursor != start_list + length as usize {
+            return Err(RlpError::InvalidFormat);
+        }
 
         let block = Block {
             hash,
             size: bytes.len(),
             header,
             transactions,
-            uncles: vec![],
+            uncles,
+            version,
+            ext_data,
         };
         Ok(block)
     }
@@ -82,8 +104,15 @@ impl Block {
         let mix_digest = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
         let nonce = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
 
-        let header = if cursor == bytes.len() {
-            Header::Legacy {
+        let header = {
+            let ext_data_hash = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let base_fee = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let ext_data_gas_used = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let block_gas_cost = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let blob_gas_used = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let excess_blob_gas = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            let parent_beacon_block_root = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
+            Header::Extra {
                 parent_hash,
                 uncle_hash,
                 coinbase,
@@ -99,160 +128,13 @@ impl Block {
                 extra,
                 mix_digest,
                 nonce,
-            }
-        } else {
-            let base_fee = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-            if cursor == bytes.len() {
-                Header::EIP1559 {
-                    parent_hash,
-                    uncle_hash,
-                    coinbase,
-                    state_root,
-                    tx_root,
-                    receipt_hash,
-                    bloom,
-                    difficulty,
-                    number,
-                    gas_limit,
-                    gas_used,
-                    time,
-                    extra,
-                    mix_digest,
-                    nonce,
-                    base_fee,
-                }
-            } else {
-                let withdrawal_root = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                if cursor == bytes.len() {
-                    Header::EIP4895 {
-                        parent_hash,
-                        uncle_hash,
-                        coinbase,
-                        state_root,
-                        tx_root,
-                        receipt_hash,
-                        bloom,
-                        difficulty,
-                        number,
-                        gas_limit,
-                        gas_used,
-                        time,
-                        extra,
-                        mix_digest,
-                        nonce,
-                        base_fee,
-                        withdrawal_root,
-                    }
-                } else {
-                    let ext_data_gas_used = withdrawal_root;
-                    if cursor == bytes.len() {
-                        let block_gas_cost = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                        Header::Apricot4 {
-                            parent_hash,
-                            uncle_hash,
-                            coinbase,
-                            state_root,
-                            tx_root,
-                            receipt_hash,
-                            bloom,
-                            difficulty,
-                            number,
-                            gas_limit,
-                            gas_used,
-                            time,
-                            extra,
-                            mix_digest,
-                            nonce,
-                            base_fee,
-                            ext_data_gas_used,
-                            block_gas_cost,
-                        }
-                    } else {
-                        let blob_gas_used = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                        let excess_blob_gas = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                        if cursor == bytes.len() {
-                            Header::EIP4844 {
-                                parent_hash,
-                                uncle_hash,
-                                coinbase,
-                                state_root,
-                                tx_root,
-                                receipt_hash,
-                                bloom,
-                                difficulty,
-                                number,
-                                gas_limit,
-                                gas_used,
-                                time,
-                                extra,
-                                mix_digest,
-                                nonce,
-                                base_fee,
-                                withdrawal_root,
-                                blob_gas_used,
-                                excess_blob_gas,
-                            }
-                        } else {
-                            let parent_beacon_block_root =
-                                Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                            if cursor == bytes.len() {
-                                Header::EIP4788 {
-                                    parent_hash,
-                                    uncle_hash,
-                                    coinbase,
-                                    state_root,
-                                    tx_root,
-                                    receipt_hash,
-                                    bloom,
-                                    difficulty,
-                                    number,
-                                    gas_limit,
-                                    gas_used,
-                                    time,
-                                    extra,
-                                    mix_digest,
-                                    nonce,
-                                    base_fee,
-                                    withdrawal_root,
-                                    blob_gas_used,
-                                    excess_blob_gas,
-                                    parent_beacon_block_root,
-                                }
-                            } else {
-                                let ext_data_hash = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                                let ext_data_gas_used =
-                                    Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
-                                if cursor != bytes.len() {
-                                    return Err(RlpError::InvalidFormat);
-                                }
-                                Header::Extra {
-                                    parent_hash,
-                                    uncle_hash,
-                                    coinbase,
-                                    state_root,
-                                    tx_root,
-                                    receipt_hash,
-                                    bloom,
-                                    difficulty,
-                                    number,
-                                    gas_limit,
-                                    gas_used,
-                                    time,
-                                    extra,
-                                    mix_digest,
-                                    nonce,
-                                    base_fee,
-                                    withdrawal_root,
-                                    blob_gas_used,
-                                    excess_blob_gas,
-                                    parent_beacon_block_root,
-                                    ext_data_hash,
-                                    ext_data_gas_used,
-                                }
-                            }
-                        }
-                    }
-                }
+                ext_data_hash,
+                base_fee,
+                ext_data_gas_used,
+                block_gas_cost,
+                blob_gas_used,
+                excess_blob_gas,
+                parent_beacon_block_root,
             }
         };
         Ok(header)
@@ -284,6 +166,63 @@ impl Block {
         }
         Ok(transactions)
     }
+
+    pub fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let mut buf = Vec::new();
+        {
+            let mut buf2 = Vec::new();
+            {
+                let mut buf3 = Vec::new();
+                buf3.append(&mut self.header.encode()?);
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                let mut buf3 = Vec::new();
+                Self::encode_transactions(&mut buf3, &self.transactions)?;
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                let mut buf3 = Vec::new();
+                Self::encode_uncles(&mut buf3, &self.uncles)?;
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                Rlp::encode_string(&mut buf2, &self.version.to_be_bytes())?;
+            }
+            {
+                Rlp::encode_normalized_string(&mut buf2, &self.ext_data)?;
+            }
+            Rlp::encode_list(&mut buf, &buf2)?;
+        }
+        Ok(buf)
+    }
+
+    fn encode_transactions(
+        buf: &mut Vec<u8>,
+        transactions: &Vec<Transaction>,
+    ) -> Result<(), RlpError> {
+        for tx in transactions {
+            match tx {
+                Transaction::Legacy { tx, .. } => {
+                    let ret2 = tx.encode()?;
+                    Rlp::encode_list(buf, &ret2)?;
+                }
+                Transaction::EIP2718 { envelope, .. } => {
+                    let ret2 = envelope.encode()?;
+                    Rlp::encode_string(buf, &ret2)?;
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn encode_uncles(buf: &mut Vec<u8>, uncles: &Vec<Header>) -> Result<(), RlpError> {
+        for uncle in uncles {
+            let ret2 = uncle.encode()?;
+            Rlp::encode_list(buf, &ret2)?;
+        }
+        Ok(())
+    }
 }
 
 struct Rlp;
@@ -371,6 +310,71 @@ impl Rlp {
         res[N - bytes.len()..].copy_from_slice(bytes);
         Ok(res)
     }
+
+    pub fn encode_list(buf: &mut Vec<u8>, bytes: &[u8]) -> Result<(), RlpError> {
+        let length = bytes.len() as u64;
+        let mut list = match length {
+            0..=55 => {
+                let mut res = vec![0xc0 + length as u8];
+                res.extend_from_slice(bytes);
+                res
+            }
+            56.. => {
+                let length_bytes = length.to_be_bytes();
+                let length_bytes = {
+                    let i = length_bytes.iter().position(|x| x != &0).unwrap();
+                    &length_bytes[i..]
+                };
+                let mut res = vec![0xf7 + length_bytes.len() as u8];
+                res.extend_from_slice(length_bytes);
+                res.extend_from_slice(bytes);
+                res
+            }
+        };
+        buf.append(&mut list);
+        Ok(())
+    }
+
+    pub fn encode_string(buf: &mut Vec<u8>, value: &[u8]) -> Result<(), RlpError> {
+        if value.len() as u64 > u64::MAX {
+            return Err(RlpError::StringTooLong);
+        }
+        let value = {
+            match value.iter().position(|x| x != &0) {
+                Some(i) => &value[i..],
+                None => &[],
+            }
+        };
+        Self::encode_normalized_string(buf, value)
+    }
+
+    pub fn encode_normalized_string(buf: &mut Vec<u8>, value: &[u8]) -> Result<(), RlpError> {
+        if value.len() as u64 > u64::MAX {
+            return Err(RlpError::StringTooLong);
+        }
+        let len = value.len();
+        let mut string = match len {
+            1 if value.first().unwrap() <= &0x7f => vec![*value.first().unwrap()],
+            0..=55 => {
+                let mut res = vec![0x80 + len as u8];
+                res.extend_from_slice(value);
+                res
+            }
+            56.. => {
+                let length_bytes = len.to_be_bytes();
+                let length_bytes = {
+                    let i = length_bytes.iter().position(|x| x != &0).unwrap();
+                    &length_bytes[i..]
+                };
+                let mut res = vec![0xb7 + length_bytes.len() as u8];
+                res.extend_from_slice(length_bytes);
+                res.extend_from_slice(value);
+                res
+            }
+        };
+        buf.append(&mut string);
+        Ok(())
+    }
 }
 
 type B32 = [u8; 32];
@@ -515,13 +519,13 @@ pub enum Header {
         extra: Vec<u8>,
         mix_digest: B32,
         nonce: Nonce,
+        ext_data_hash: B32,
         base_fee: U256,
-        withdrawal_root: U256,
+        ext_data_gas_used: U256,
+        block_gas_cost: U256,
         blob_gas_used: U64,
         excess_blob_gas: U64,
         parent_beacon_block_root: B32,
-        ext_data_hash: B32,
-        ext_data_gas_used: U256,
     },
 }
 
@@ -557,6 +561,60 @@ impl Header {
     make_helper!(extra, Vec<u8>);
     make_helper!(mix_digest, B32);
     make_helper!(nonce, Nonce);
+
+    fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let mut buf = Vec::new();
+        let Header::Extra {
+            parent_hash,
+            uncle_hash,
+            coinbase,
+            state_root,
+            tx_root,
+            receipt_hash,
+            bloom,
+            difficulty,
+            number,
+            gas_limit,
+            gas_used,
+            time,
+            extra,
+            mix_digest,
+            nonce,
+            ext_data_hash,
+            base_fee,
+            ext_data_gas_used,
+            block_gas_cost,
+            blob_gas_used,
+            excess_blob_gas,
+            parent_beacon_block_root,
+        } = self
+        else {
+            panic!()
+        };
+        Rlp::encode_normalized_string(&mut buf, parent_hash)?;
+        Rlp::encode_normalized_string(&mut buf, uncle_hash)?;
+        Rlp::encode_string(&mut buf, coinbase)?;
+        Rlp::encode_normalized_string(&mut buf, state_root)?;
+        Rlp::encode_normalized_string(&mut buf, tx_root)?;
+        Rlp::encode_normalized_string(&mut buf, receipt_hash)?;
+        Rlp::encode_normalized_string(&mut buf, bloom)?;
+        Rlp::encode_string(&mut buf, difficulty)?;
+        Rlp::encode_string(&mut buf, number)?;
+        Rlp::encode_string(&mut buf, gas_limit)?;
+        Rlp::encode_string(&mut buf, gas_used)?;
+        Rlp::encode_string(&mut buf, time)?;
+        Rlp::encode_normalized_string(&mut buf, extra)?;
+        Rlp::encode_normalized_string(&mut buf, mix_digest)?;
+        Rlp::encode_normalized_string(&mut buf, nonce)?;
+        Rlp::encode_normalized_string(&mut buf, ext_data_hash)?;
+        Rlp::encode_string(&mut buf, base_fee)?;
+        Rlp::encode_string(&mut buf, ext_data_gas_used)?;
+        Rlp::encode_string(&mut buf, block_gas_cost)?;
+        Rlp::encode_string(&mut buf, blob_gas_used)?;
+        Rlp::encode_string(&mut buf, excess_blob_gas)?;
+        Rlp::encode_normalized_string(&mut buf, parent_beacon_block_root)?;
+        Ok(buf)
+    }
 }
 
 impl From<Header> for alloy::consensus::Header {
@@ -631,6 +689,35 @@ impl TransactionLegacy {
             s,
         })
     }
+
+    pub fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let TransactionLegacy {
+            nonce,
+            gas_price,
+            gas_limit,
+            to,
+            value,
+            data,
+            v,
+            r,
+            s,
+        } = self;
+        let mut bytes = Vec::new();
+        Rlp::encode_string(&mut bytes, nonce)?;
+        Rlp::encode_string(&mut bytes, gas_price)?;
+        Rlp::encode_string(&mut bytes, gas_limit)?;
+        if to == &[0; 20] {
+            Rlp::encode_string(&mut bytes, &[])?;
+        } else {
+            Rlp::encode_normalized_string(&mut bytes, to)?;
+        }
+        Rlp::encode_string(&mut bytes, value)?;
+        Rlp::encode_string(&mut bytes, data)?;
+        Rlp::encode_string(&mut bytes, v)?;
+        Rlp::encode_string(&mut bytes, r)?;
+        Rlp::encode_string(&mut bytes, s)?;
+        Ok(bytes)
+    }
 }
 
 impl TransactionEnvelope {
@@ -771,6 +858,92 @@ impl TransactionEnvelope {
 
         Ok(envelope)
     }
+
+    pub fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let mut ret2 = Vec::new();
+        match self {
+            TransactionEnvelope::AccessList(TransactionAccessList {
+                chain_id,
+                nonce,
+                gas_price,
+                gas_limit,
+                to,
+                value,
+                data,
+                access_list,
+                v,
+                r,
+                s,
+            }) => {
+                ret2.push(0x01);
+                {
+                    let mut ret3 = Vec::new();
+                    Rlp::encode_string(&mut ret3, chain_id)?;
+                    Rlp::encode_string(&mut ret3, nonce)?;
+                    Rlp::encode_string(&mut ret3, gas_price)?;
+                    Rlp::encode_string(&mut ret3, gas_limit)?;
+                    if to == &[0; 20] {
+                        Rlp::encode_string(&mut ret3, &[])?;
+                    } else {
+                        Rlp::encode_normalized_string(&mut ret3, to)?;
+                    }
+                    Rlp::encode_string(&mut ret3, value)?;
+                    Rlp::encode_string(&mut ret3, data)?;
+                    {
+                        let mut ret4 = Vec::new();
+                        ret4.append(&mut access_list.encode()?);
+                        Rlp::encode_list(&mut ret3, &ret4)?;
+                    }
+                    Rlp::encode_string(&mut ret3, v)?;
+                    Rlp::encode_string(&mut ret3, r)?;
+                    Rlp::encode_string(&mut ret3, s)?;
+                    Rlp::encode_list(&mut ret2, &ret3)?;
+                }
+            }
+            TransactionEnvelope::DynamicFee(TransactionDynamicFee {
+                chain_id,
+                nonce,
+                max_priority_fee_per_gas,
+                max_fee_per_gas,
+                gas_limit,
+                destination,
+                amount,
+                data,
+                access_list,
+                v,
+                r,
+                s,
+            }) => {
+                ret2.push(0x02);
+                {
+                    let mut ret3 = Vec::new();
+                    Rlp::encode_string(&mut ret3, chain_id)?;
+                    Rlp::encode_string(&mut ret3, nonce)?;
+                    Rlp::encode_string(&mut ret3, max_priority_fee_per_gas)?;
+                    Rlp::encode_string(&mut ret3, max_fee_per_gas)?;
+                    Rlp::encode_string(&mut ret3, gas_limit)?;
+                    if destination == &[0; 20] {
+                        Rlp::encode_string(&mut ret3, &[])?;
+                    } else {
+                        Rlp::encode_normalized_string(&mut ret3, destination)?;
+                    }
+                    Rlp::encode_string(&mut ret3, amount)?;
+                    Rlp::encode_string(&mut ret3, data)?;
+                    {
+                        let mut ret4 = Vec::new();
+                        ret4.append(&mut access_list.encode()?);
+                        Rlp::encode_list(&mut ret3, &ret4)?;
+                    }
+                    Rlp::encode_string(&mut ret3, v)?;
+                    Rlp::encode_string(&mut ret3, r)?;
+                    Rlp::encode_string(&mut ret3, s)?;
+                    Rlp::encode_list(&mut ret2, &ret3)?;
+                }
+            }
+            _ => todo!(),
+        }
+        Ok(ret2)
+    }
 }
 
 impl From<AccessList> for alloy::eips::eip2930::AccessList {
@@ -1095,19 +1268,41 @@ impl AccessList {
             Ok(AccessList(access_list))
         }
     }
+
+    pub fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let Self(access_list) = self;
+        if access_list.is_empty() {
+            return Ok(vec![]);
+        }
+        let mut buf = Vec::new();
+        for item in &self.0 {
+            Rlp::encode_normalized_string(&mut buf, &item.address)?;
+            let mut storage_keys = Vec::new();
+            for storage_key in &item.storage_keys {
+                Rlp::encode_normalized_string(&mut storage_keys, storage_key)?;
+            }
+            Rlp::encode_list(&mut buf, &storage_keys)?;
+        }
+        let mut result = Vec::new();
+        Rlp::encode_list(&mut result, &buf)?;
+        Ok(result)
+    }
 }
 
 #[cfg(test)]
 mod tests {
+    use super::*;
     use crate::utils::unpacker::StatelessBlock;
+    use pretty_assertions::assert_eq;
     use std::fmt::Debug;
     use std::fs;
 
     const TEST_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/");
 
-    fn can_parse<F, OK, ERR>(dir: &str, f: F)
+    fn can_parse<PARSEF, UNPARSEF, OK, ERR>(dir: &str, parse: PARSEF, unparse: UNPARSEF)
     where
-        F: Fn(&[u8]) -> Result<OK, ERR>,
+        PARSEF: Fn(&[u8]) -> Result<OK, ERR>,
+        UNPARSEF: Fn(&OK) -> Result<Vec<u8>, ERR>,
         ERR: Debug,
     {
         let test_dir = TEST_DIR.to_owned() + dir;
@@ -1116,11 +1311,12 @@ mod tests {
             if path.is_file() && path.extension().unwrap() == "bin" {
                 let file_name = path.file_name().unwrap().to_str().unwrap();
                 println!("{}....", file_name);
-
                 let bytes = fs::read(&path).unwrap();
                 assert!(!bytes.is_empty());
 
-                f(&bytes).unwrap();
+                let res = parse(&bytes).unwrap();
+                let ret_bytes = unparse(&res).unwrap();
+                assert_eq!(ret_bytes, bytes);
                 println!("ok!");
             } else {
                 panic!("invalid path {:?}", path);
@@ -1130,11 +1326,35 @@ mod tests {
 
     #[test]
     fn can_parse_block() {
-        can_parse("blocks", StatelessBlock::unpack);
+        can_parse("blocks", StatelessBlock::unpack, StatelessBlock::pack);
     }
 
-    // #[test]
-    // fn can_parse_transaction() {
-    //     can_parse("txs", Transaction::decode);
-    // }
+    #[test]
+    fn can_parse_transaction() {
+        fn decode(bytes: &[u8]) -> Result<Transaction, RlpError> {
+            if bytes.is_empty() {
+                return Err(RlpError::InputTooShort);
+            }
+            let prefix = bytes.first().unwrap();
+            let transaction = match prefix {
+                0x00..=0x7f => Transaction::EIP2718 {
+                    envelope: TransactionEnvelope::decode(bytes)?,
+                    hash: Default::default(),
+                },
+                0x80.. => Transaction::Legacy {
+                    tx: TransactionLegacy::decode(bytes)?,
+                    hash: Default::default(),
+                },
+            };
+            Ok(transaction)
+        }
+
+        fn encode(transaction: &Transaction) -> Result<Vec<u8>, RlpError> {
+            match transaction {
+                Transaction::Legacy { tx, .. } => tx.encode(),
+                Transaction::EIP2718 { envelope, .. } => envelope.encode(),
+            }
+        }
+        can_parse("txs", decode, encode);
+    }
 }
diff --git a/node/src/utils/unpacker.rs b/node/src/utils/unpacker.rs
index db8dfe7..04465d1 100644
--- a/node/src/utils/unpacker.rs
+++ b/node/src/utils/unpacker.rs
@@ -1,4 +1,4 @@
-use super::packer::PackerError;
+use super::packer::{Packer, PackerError};
 use crate::id::BlockID;
 use crate::utils::rlp::Block;
 use sha2::Digest;
@@ -43,17 +43,12 @@ impl Unpacker {
 pub struct StatelessBlock {
     #[allow(unused)]
     pub version: Option<Vec<u8>>,
-    #[allow(unused)]
     pub parent_id: BlockID,
-    #[allow(unused)]
     pub timestamp: u64,
-    #[allow(unused)]
     pub p_chain_height: u64,
-    #[allow(unused)]
     pub certificate: Vec<u8>,
     pub block: Block,
-    pub id: BlockID,
-    #[allow(unused)]
+    id: BlockID,
     pub sig: Vec<u8>,
 }
 
@@ -114,4 +109,20 @@ impl StatelessBlock {
             sig: sig_bytes.to_vec(),
         })
     }
+
+    pub fn id(&self) -> &BlockID {
+        &self.id
+    }
+
+    pub fn pack(&self) -> Result<Vec<u8>, PackerError> {
+        let mut packer = Packer::new();
+        packer.pack_fixed_bytes(&[0, 0, 0, 0, 0, 0]);
+        packer.pack_fixed_bytes(self.parent_id.as_ref());
+        packer.pack_fixed_bytes(&self.timestamp.to_be_bytes());
+        packer.pack_fixed_bytes(&self.p_chain_height.to_be_bytes());
+        packer.pack_bytes(&self.certificate);
+        packer.pack_bytes(&self.block.encode()?);
+        packer.pack_bytes(&self.sig);
+        Ok(packer.finish())
+    }
 }
diff --git a/proto/sdk.proto b/proto/sdk.proto
index 7b8234c..c5a6fb1 100644
--- a/proto/sdk.proto
+++ b/proto/sdk.proto
@@ -24,6 +24,8 @@ message LightRequest {
 
     FindValue find_value = 11;
     FindNode find_node = 12;
+
+    Store store = 13;
   }
 }
 
@@ -50,6 +52,14 @@ message FindNode {
   bytes bucket = 1;
 }
 
+// Store is used to store content to a distant peer
+//
+// Works as defined in the Kademlia DHT paper: https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf
+message Store {
+  uint32 dht_id = 1;
+  bytes value = 2;
+}
+
 message LightResponse {
   oneof message {
     Ack ack = 1;
diff --git a/proto/src/lib.rs b/proto/src/lib.rs
index 333d503..53b02d4 100644
--- a/proto/src/lib.rs
+++ b/proto/src/lib.rs
@@ -14,6 +14,12 @@ impl From<sdk::FindNode> for sdk::light_request::Message {
     }
 }
 
+impl From<sdk::Store> for sdk::light_request::Message {
+    fn from(message: sdk::Store) -> Self {
+        Self::Store(message)
+    }
+}
+
 impl From<sdk::Value> for sdk::light_response::Message {
     fn from(message: sdk::Value) -> Self {
         Self::Value(message)
diff --git a/proto/src/sdk.rs b/proto/src/sdk.rs
index 027ce32..8e9cb80 100644
--- a/proto/src/sdk.rs
+++ b/proto/src/sdk.rs
@@ -18,7 +18,7 @@ pub struct PushGossip {
 }
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct LightRequest {
-    #[prost(oneof = "light_request::Message", tags = "1, 11, 12")]
+    #[prost(oneof = "light_request::Message", tags = "1, 11, 12, 13")]
     pub message: ::core::option::Option<light_request::Message>,
 }
 /// Nested message and enum types in `LightRequest`.
@@ -31,6 +31,8 @@ pub mod light_request {
         FindValue(super::FindValue),
         #[prost(message, tag = "12")]
         FindNode(super::FindNode),
+        #[prost(message, tag = "13")]
+        Store(super::Store),
     }
 }
 #[derive(Clone, PartialEq, ::prost::Message)]
@@ -61,6 +63,16 @@ pub struct FindNode {
     #[prost(bytes = "vec", tag = "1")]
     pub bucket: ::prost::alloc::vec::Vec<u8>,
 }
+/// Store is used to store content to a distant peer
+///
+/// Works as defined in the Kademlia DHT paper: <https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf>
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct Store {
+    #[prost(uint32, tag = "1")]
+    pub dht_id: u32,
+    #[prost(bytes = "vec", tag = "2")]
+    pub value: ::prost::alloc::vec::Vec<u8>,
+}
 #[derive(Clone, PartialEq, ::prost::Message)]
 pub struct LightResponse {
     #[prost(oneof = "light_response::Message", tags = "1, 2, 3")]


Reviews:
user: login: iFrostizz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-05T03:39:57Z
user: login: iFrostizz
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-05T19:01:34Z

Review comments:
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
+            Ok(())
+        } else {
+            self.kademlia_dht.store(node_id, &DHT::id(), encoded).await
+        }
+    }
+
+    // TODO: those dht-specific functions should be placed outside of this struct.
     pub async fn find_block(&self, number: u64) -> Result<Block, LightError> {
         self.find_content(&self.block_dht, number)
             .await
             .map(|stateless_block| stateless_block.block)
     }
+
+    pub async fn store_block(
+        &self,
+        node_id: NodeId,
+        block: StatelessBlock,
+    ) -> Result<(), LightError> {
+        self.store(&self.block_dht, node_id, block).await
+    }
+
+    pub async fn ping(&self, node_id: NodeId) -> Result<(), LightError> {
+        if node_id == self.light_peers.node_id {
+            return Err(light_errors::SEND_TO_SELF);
+        }
+        let maybe_peer_infos = {
+            let peers_infos = self.kademlia_dht.peers_infos.read().unwrap();
+            peers_infos.get(&node_id).cloned()
+        };
+        if let Some(peer_infos) = maybe_peer_infos {
+            peer_infos
+                .ping(&self.kademlia_dht.mail_tx)
+                .await
+                .map_err(|_todo_err| LightError {
+                    code: 8,
+                    message: "TODO",
+                })
path: node/src/net/light.rs
user: login: iFrostizz
body: Find a way to convert errors here
created_at: 2025-05-04T10:10:43Z
author_association: OWNER
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
+            Ok(())
+        } else {
+            self.kademlia_dht.store(node_id, &DHT::id(), encoded).await
+        }
+    }
path: node/src/net/light.rs
user: login: iFrostizz
body: Move this function out of the struct, and put in the rpc.
created_at: 2025-05-04T10:11:29Z
author_association: OWNER
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
+            Ok(())
+        } else {
+            self.kademlia_dht.store(node_id, &DHT::id(), encoded).await
+        }
+    }
+
+    // TODO: those dht-specific functions should be placed outside of this struct.
     pub async fn find_block(&self, number: u64) -> Result<Block, LightError> {
         self.find_content(&self.block_dht, number)
             .await
             .map(|stateless_block| stateless_block.block)
     }
+
+    pub async fn store_block(
+        &self,
+        node_id: NodeId,
+        block: StatelessBlock,
+    ) -> Result<(), LightError> {
+        self.store(&self.block_dht, node_id, block).await
+    }
+
+    pub async fn ping(&self, node_id: NodeId) -> Result<(), LightError> {
+        if node_id == self.light_peers.node_id {
+            return Err(light_errors::SEND_TO_SELF);
+        }
+        let maybe_peer_infos = {
+            let peers_infos = self.kademlia_dht.peers_infos.read().unwrap();
+            peers_infos.get(&node_id).cloned()
+        };
+        if let Some(peer_infos) = maybe_peer_infos {
+            peer_infos
+                .ping(&self.kademlia_dht.mail_tx)
+                .await
+                .map_err(|_todo_err| LightError {
+                    code: 8,
+                    message: "TODO",
+                })
path: node/src/net/light.rs
user: login: iFrostizz
body: Those should be in the rpc
created_at: 2025-05-04T10:16:57Z
author_association: OWNER
diff_hunk: @@ -630,6 +631,15 @@ impl Peer {
         log::trace!("received light message {light_message:?}");
         let chain_id = c_chain_id.as_ref().to_vec();
         let res = match light_message {
+            sdk::light_request::Message::Store(Store { dht_id, value }) => {
+                let dht_id = match dht_id {
+                    0 => DhtId::Block,
+                    _ => return Err(NodeError::Message("unsupported DHT".to_string())),
+                };
path: node/src/net/mod.rs
user: login: iFrostizz
body: Use TryFrom here
created_at: 2025-05-04T10:17:30Z
author_association: OWNER
diff_hunk: @@ -611,11 +664,236 @@ mod rpc_impl {
             not_implemented!()
         }
     }
+
+    #[async_trait]
+    impl LightServer for RpcServerImpl {
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()> {
+            self.node.light_network.ping(node_id).await?;
+            Ok(())
+        }
+
+        async fn store(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            value: Bytes,
+        ) -> RpcResult<()> {
+            let node_id = if node_id.is_none() {
+                self.node.network.node_id
+            } else {
+                node_id.unwrap()
+            };
+            match dht_id {
+                DhtId::Block => {
+                    let block = DhtBlocks::decode(&value)?;
+                    self.node.light_network.store_block(node_id, block).await?;
+                }
+                DhtId::State => {}
+            }
+            Ok(())
+        }
+
+        async fn find_node(
+            &self,
+            node_id: Option<NodeId>,
+            bucket: Bucket,
+        ) -> RpcResult<Vec<NodeId>> {
+            let node_ids = if node_id.is_none() || node_id == Some(self.node.network.node_id) {
+                self.node
+                    .light_network
+                    .find_node(&bucket)
+                    .into_iter()
+                    .map(|ConnectionData { node_id, .. }| node_id)
+                    .collect()
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindNode {
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let rx = sender
+                    .send_and_response(
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .unwrap(); // TODO: error handling
path: node/src/server/rpc.rs
user: login: iFrostizz
body: Error handling.
created_at: 2025-05-04T10:17:58Z
author_association: OWNER
diff_hunk: @@ -611,11 +664,236 @@ mod rpc_impl {
             not_implemented!()
         }
     }
+
+    #[async_trait]
+    impl LightServer for RpcServerImpl {
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()> {
+            self.node.light_network.ping(node_id).await?;
+            Ok(())
+        }
+
+        async fn store(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            value: Bytes,
+        ) -> RpcResult<()> {
+            let node_id = if node_id.is_none() {
+                self.node.network.node_id
+            } else {
+                node_id.unwrap()
+            };
+            match dht_id {
+                DhtId::Block => {
+                    let block = DhtBlocks::decode(&value)?;
+                    self.node.light_network.store_block(node_id, block).await?;
+                }
+                DhtId::State => {}
+            }
+            Ok(())
+        }
+
+        async fn find_node(
+            &self,
+            node_id: Option<NodeId>,
+            bucket: Bucket,
+        ) -> RpcResult<Vec<NodeId>> {
+            let node_ids = if node_id.is_none() || node_id == Some(self.node.network.node_id) {
+                self.node
+                    .light_network
+                    .find_node(&bucket)
+                    .into_iter()
+                    .map(|ConnectionData { node_id, .. }| node_id)
+                    .collect()
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindNode {
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let rx = sender
+                    .send_and_response(
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .unwrap(); // TODO: error handling
+
+                // TODO: error handling
+                // let p2p::message::Message::AppResponse(app_response) = rx.await? else {
+                let p2p::message::Message::AppResponse(app_response) = rx.await.unwrap() else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                if app_response.chain_id
+                    != self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .chain_id
+                        .as_ref()
+                        .to_vec()
+                {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                }
path: node/src/server/rpc.rs
user: login: iFrostizz
body: Validation logic is very duplicated, write a function.
created_at: 2025-05-04T10:18:17Z
author_association: OWNER
diff_hunk: @@ -95,6 +96,12 @@ impl From<&DhtId> for u32 {
     }
 }
 
path: node/src/dht/mod.rs
user: login: iFrostizz
body: Also implement TryFrom<u32>
created_at: 2025-05-05T03:12:05Z
author_association: OWNER
diff_hunk: @@ -38,6 +49,25 @@ impl AsRef<[u8]> for BlockID {
     }
 }
 
+impl Serialize for BlockID {
path: node/src/id/block.rs
user: login: iFrostizz
body: Not needed anymore?
created_at: 2025-05-05T03:13:12Z
author_association: OWNER
diff_hunk: @@ -134,7 +134,7 @@ impl LightNetwork {
     }
 
     /// Lookup locally for nodes spanning the bucket.
-    fn find_node(&self, bucket: &Bucket) -> Vec<ConnectionData> {
+    pub fn find_node(&self, bucket: &Bucket) -> Vec<ConnectionData> {
         self.kademlia_dht.find_node(bucket)
path: node/src/net/light.rs
user: login: iFrostizz
body: Unneeded
created_at: 2025-05-05T03:15:03Z
author_association: OWNER
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
path: node/src/net/light.rs
user: login: iFrostizz
body: This is app-specific. 
The node ID matching should be done at a higher level. Maybe think about separating client functions (call to other nodes) and local functions (call to database, cache, ...)
created_at: 2025-05-05T03:17:15Z
author_association: OWNER
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
+            Ok(())
+        } else {
+            self.kademlia_dht.store(node_id, &DHT::id(), encoded).await
+        }
+    }
+
+    // TODO: those dht-specific functions should be placed outside of this struct.
path: node/src/net/light.rs
user: login: iFrostizz
body: TODO
created_at: 2025-05-05T03:17:44Z
author_association: OWNER
diff_hunk: @@ -368,6 +381,46 @@ mod rpc_impl {
         fn get_logs(&self, filter_object: FilterObject) -> RpcResult<Vec<LogObject>>;
     }
 
+    #[derive(Serialize, Clone)]
+    pub enum Value {
+        Block(alloy::rpc::types::Block),
+    }
+
+    #[derive(Serialize, Clone)]
+    pub enum RpcValueOrNodes {
+        Value(Value),
+        Nodes(Vec<NodeId>),
+    }
+
+    #[rpc(server, namespace = "light")]
+    pub trait Light {
+        #[method(name = "ping")]
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()>;
+
+        #[method(name = "store")]
+        async fn store(
path: node/src/server/rpc.rs
user: login: iFrostizz
body: The Node ID matching should be done at the rpc level (with a helper function) and the actual message call at a lower level (in the light client)
created_at: 2025-05-05T03:23:56Z
author_association: OWNER
diff_hunk: @@ -611,11 +664,236 @@ mod rpc_impl {
             not_implemented!()
         }
     }
+
+    #[async_trait]
+    impl LightServer for RpcServerImpl {
+        async fn ping(&self, node_id: NodeId) -> RpcResult<()> {
+            self.node.light_network.ping(node_id).await?;
+            Ok(())
+        }
+
+        async fn store(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            value: Bytes,
+        ) -> RpcResult<()> {
+            let node_id = if node_id.is_none() {
+                self.node.network.node_id
+            } else {
+                node_id.unwrap()
+            };
+            match dht_id {
+                DhtId::Block => {
+                    let block = DhtBlocks::decode(&value)?;
+                    self.node.light_network.store_block(node_id, block).await?;
+                }
+                DhtId::State => {}
+            }
+            Ok(())
+        }
+
+        async fn find_node(
+            &self,
+            node_id: Option<NodeId>,
+            bucket: Bucket,
+        ) -> RpcResult<Vec<NodeId>> {
+            let node_ids = if node_id.is_none() || node_id == Some(self.node.network.node_id) {
+                self.node
+                    .light_network
+                    .find_node(&bucket)
+                    .into_iter()
+                    .map(|ConnectionData { node_id, .. }| node_id)
+                    .collect()
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindNode {
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let rx = sender
+                    .send_and_response(
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .unwrap(); // TODO: error handling
+
+                // TODO: error handling
+                // let p2p::message::Message::AppResponse(app_response) = rx.await? else {
+                let p2p::message::Message::AppResponse(app_response) = rx.await.unwrap() else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                if app_response.chain_id
+                    != self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .chain_id
+                        .as_ref()
+                        .to_vec()
+                {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                }
+                let bytes = app_response.app_bytes;
+                let Ok((app_id, bytes)) = unsigned_varint::decode::u64(&bytes) else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                if app_id != constants::SNOWFLAKE_HANDLER_ID {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                }
+                let Ok(light_message) = InboundMessage::decode(bytes) else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                match light_message {
+                    sdk::light_response::Message::Nodes(p2p::PeerList { claimed_ip_ports }) => {
+                        if claimed_ip_ports.len() > 10 {
+                            return Err(light_errors::INVALID_CONTENT.into());
+                        }
+                        claimed_ip_ports
+                            .into_iter()
+                            .filter_map(|claimed_ip_port| claimed_ip_port.try_into().ok())
+                            .map(|ConnectionData { node_id, .. }| node_id)
+                            .collect()
+                    }
+                    _ => return Err(light_errors::INVALID_CONTENT.into()),
+                }
+            };
+            Ok(node_ids)
+        }
+
+        async fn find_value(
+            &self,
+            node_id: Option<NodeId>,
+            dht_id: DhtId,
+            bucket: Bucket,
+        ) -> RpcResult<RpcValueOrNodes> {
+            let value_or_nodes = if node_id.is_none() || node_id == Some(self.node.network.node_id)
+            {
+                match dht_id {
+                    DhtId::Block => {
+                        let store = self.node.light_network.block_dht.store.read().unwrap();
+                        match store.get(&bucket) {
+                            Some(value) => RpcValueOrNodes::Value(Value::Block(block_to_rpc(
+                                DhtBlocks::decode(value)?.block,
+                                true,
+                            ))),
+                            None => {
+                                let connections_data = self.node.light_network.find_node(&bucket);
+                                let node_ids = connections_data
+                                    .into_iter()
+                                    .map(|ConnectionData { node_id, .. }| node_id)
+                                    .collect();
+                                RpcValueOrNodes::Nodes(node_ids)
+                            }
+                        }
+                    }
+                    _ => return Err(light_errors::INVALID_DHT.into()),
+                }
+            } else {
+                let node_id = node_id.unwrap();
+                let sender = {
+                    let peers_infos = self
+                        .node
+                        .light_network
+                        .kademlia_dht
+                        .peers_infos
+                        .read()
+                        .unwrap();
+                    let Some(peer_infos) = peers_infos.get(&node_id) else {
+                        return Err(light_errors::PEER_MISSING.into());
+                    };
+                    peer_infos.sender.clone()
+                };
+                let message = AppRequestMessage::encode(
+                    &self.node.light_network.kademlia_dht.chain_id,
+                    sdk::FindValue {
+                        dht_id: (&dht_id).into(), // TODO: how to implement an auto AsRef?
+                        bucket: bucket.to_be_bytes_vec(),
+                    },
+                );
+                let Ok(p2p::message::Message::AppRequest(app_request)) = message else {
+                    return Err(light_errors::INVALID_CONTENT.into());
+                };
+                let rx = sender
+                    .send_and_response(
+                        &self.node.light_network.kademlia_dht.mail_tx,
+                        SubscribableMessage::AppRequest(app_request),
+                    )
+                    .unwrap(); // TODO: error handling
path: node/src/server/rpc.rs
user: login: iFrostizz
body: Error handling
created_at: 2025-05-05T03:31:19Z
author_association: OWNER
diff_hunk: @@ -82,8 +104,187 @@ impl Block {
         let mix_digest = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
         let nonce = Rlp::decode_fixed_bytes(bytes, &mut cursor)?;
 
-        let header = if cursor == bytes.len() {
-            Header::Legacy {
+        // let header = if cursor == bytes.len() {
path: node/src/utils/rlp.rs
user: login: iFrostizz
body: Remove these comments and only decode the extra header. 
created_at: 2025-05-05T03:34:29Z
author_association: OWNER
diff_hunk: @@ -284,6 +338,134 @@ impl Block {
         }
         Ok(transactions)
     }
+
+    pub fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        // TODO this function is allocating way too much!
+        let mut buf = Vec::new();
+        {
+            let mut buf2 = Vec::new();
+            {
+                let mut buf3 = Vec::new();
+                buf3.append(&mut self.header.encode()?);
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                let mut buf3 = Vec::new();
+                Self::encode_transactions(&mut buf3, &self.transactions)?;
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                let mut buf3 = Vec::new();
+                Self::encode_uncles(&mut buf3, &self.uncles)?;
+                Rlp::encode_list(&mut buf2, &buf3)?;
+            }
+            {
+                Rlp::encode_string(&mut buf2, &self.version.to_be_bytes())?;
+            }
+            {
+                Rlp::encode_normalized_string(&mut buf2, &self.ext_data)?;
+            }
+            Rlp::encode_list(&mut buf, &buf2)?;
+        }
+        Ok(buf)
+    }
+
+    fn encode_transactions(
+        buf: &mut Vec<u8>,
+        transactions: &Vec<Transaction>,
+    ) -> Result<(), RlpError> {
+        for tx in transactions {
+            match tx {
+                Transaction::Legacy {
+                    tx:
+                        TransactionLegacy {
+                            nonce,
+                            gas_price,
+                            gas_limit,
+                            to,
+                            value,
+                            data,
+                            v,
+                            r,
+                            s,
+                        },
+                    ..
+                } => {
+                    let mut ret2 = Vec::new();
+                    Rlp::encode_string(&mut ret2, nonce)?;
+                    Rlp::encode_string(&mut ret2, gas_price)?;
+                    Rlp::encode_string(&mut ret2, gas_limit)?;
+                    if to == &[0; 20] {
+                        Rlp::encode_string(&mut ret2, &[])?;
+                    } else {
+                        Rlp::encode_normalized_string(&mut ret2, to)?;
+                    }
+                    Rlp::encode_string(&mut ret2, value)?;
+                    Rlp::encode_string(&mut ret2, data)?;
+                    Rlp::encode_string(&mut ret2, v)?;
+                    Rlp::encode_string(&mut ret2, r)?;
+                    Rlp::encode_string(&mut ret2, s)?;
+                    Rlp::encode_list(buf, &ret2)?;
+                }
+                Transaction::EIP2718 { envelope, .. } => {
+                    let mut ret2 = Vec::new();
+                    match envelope {
+                        TransactionEnvelope::DynamicFee(TransactionDynamicFee {
+                            chain_id,
+                            nonce,
+                            max_priority_fee_per_gas,
+                            max_fee_per_gas,
+                            gas_limit,
+                            destination,
+                            amount,
+                            data,
+                            access_list,
+                            v,
+                            r,
+                            s,
+                        }) => {
+                            ret2.push(0x02);
+                            {
+                                let mut ret3 = Vec::new();
+                                Rlp::encode_string(&mut ret3, chain_id)?;
+                                Rlp::encode_string(&mut ret3, nonce)?;
+                                Rlp::encode_string(&mut ret3, max_priority_fee_per_gas)?;
+                                Rlp::encode_string(&mut ret3, max_fee_per_gas)?;
+                                Rlp::encode_string(&mut ret3, gas_limit)?;
+                                if destination == &[0; 20] {
+                                    Rlp::encode_string(&mut ret3, &[])?;
+                                } else {
+                                    Rlp::encode_normalized_string(&mut ret3, destination)?;
+                                }
+                                Rlp::encode_string(&mut ret3, amount)?;
+                                Rlp::encode_string(&mut ret3, data)?;
+                                {
+                                    let mut ret4 = Vec::new();
+                                    ret4.append(&mut access_list.encode()?);
+                                    Rlp::encode_list(&mut ret3, &ret4)?;
+                                }
+                                Rlp::encode_string(&mut ret3, v)?;
+                                Rlp::encode_string(&mut ret3, r)?;
+                                Rlp::encode_string(&mut ret3, s)?;
+                                Rlp::encode_list(&mut ret2, &ret3)?;
+                            }
+                        }
+                        _ => todo!(),
path: node/src/utils/rlp.rs
user: login: iFrostizz
body: Implement other tx types
created_at: 2025-05-05T03:35:18Z
author_association: OWNER
diff_hunk: @@ -557,6 +804,276 @@ impl Header {
     make_helper!(extra, Vec<u8>);
     make_helper!(mix_digest, B32);
     make_helper!(nonce, Nonce);
+
+    fn encode(&self) -> Result<Vec<u8>, RlpError> {
+        let mut buf = Vec::new();
+        // match self {
path: node/src/utils/rlp.rs
user: login: iFrostizz
body: Remove these comments and only have the extra header 
created_at: 2025-05-05T03:36:55Z
author_association: OWNER
diff_hunk: @@ -1130,7 +1669,7 @@ mod tests {
 
     #[test]
     fn can_parse_block() {
-        can_parse("blocks", StatelessBlock::unpack);
+        can_parse("blocks", StatelessBlock::unpack, StatelessBlock::pack);
     }
 
path: node/src/utils/rlp.rs
user: login: iFrostizz
body: Make the can_parse_transactions test pass again.
created_at: 2025-05-05T03:39:30Z
author_association: OWNER
diff_hunk: @@ -177,11 +177,59 @@ impl LightNetwork {
         }
     }
 
+    async fn store<DHT, K, V>(
+        &self,
+        dht: &Arc<DHT>,
+        node_id: NodeId,
+        value: V,
+    ) -> Result<(), LightError>
+    where
+        DHT: ConcreteDht<K> + DhtContent<K, V>,
+    {
+        let encoded = DHT::encode(value)?;
+        if node_id == self.light_peers.node_id {
+            dht.insert_to_store(encoded)?;
path: node/src/net/light.rs
user: login: iFrostizz
body: Fine because we would have to handle the node ID anyway by i.e. returning an error.
created_at: 2025-05-05T19:01:34Z
author_association: OWNER

PR comments:
user: login: iFrostizz
created_at: 2025-05-05T01:06:44Z
body: In the send_and_response function, return a generic parameter for the message type. 
author_association: OWNER
user: login: iFrostizz
created_at: 2025-05-05T01:10:10Z
body: Think about writing a helper function to send a message and get the response. 
author_association: OWNER
user: login: iFrostizz
created_at: 2025-05-06T05:42:13Z
body: Will merge and then work on https://github.com/iFrostizz/snowflake/issues/45
author_association: OWNER