<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>0xndavd</title><description>ndavd&apos;s blog</description><link>https://ndavd.eth.limo/</link><item><title>Hello, (InterPlanetary) World</title><link>https://ndavd.eth.limo/hello-world/</link><guid isPermaLink="true">https://ndavd.eth.limo/hello-world/</guid><description>the first post; deep-diving into IPFS, IPLD &amp; IPNS;</description><pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few days ago, when I first decided to create this technical blog, the first
post was supposed to be about something entirely different. But also during that
time, I decided that I wanted to host it in a decentralized manner, using the
&lt;strong&gt;InterPlanetary File System&lt;/strong&gt; (&lt;a href=&quot;https://ipfs.io&quot;&gt;IPFS&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I had worked with IPFS before, used it to host metadata and assets for tokens.
Now, hosting a whole blog there, managing CD (continuous deployment) and keeping
it synced with my ENS, introduced some challenges which led me to dive deep into
how it works... And that&apos;s what I&apos;m talking about today.&lt;/p&gt;
&lt;p&gt;Let&apos;s start from the beginning:&lt;/p&gt;
&lt;h4&gt;What&apos;s IPFS and why would I want to host there?&lt;/h4&gt;
&lt;p&gt;The IPFS is a protocol that manages a distributed &lt;em&gt;File System&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Essentially, it&apos;s decentralized storage.&lt;/p&gt;
&lt;p&gt;And the reason why it&apos;s called &lt;em&gt;InterPlanetary&lt;/em&gt; is because it has been
envisioned as a protocol fit for data sharing over long distances, and one day,
perhaps, even between planets (it&apos;s already
&lt;a href=&quot;https://thedefiant.io/news/defi/filecoin-ipfs-space&quot;&gt;running in space&lt;/a&gt; with the
help of the Filecoin Foundation and Lockheed Martin Space): It doesn&apos;t matter
where the data is uploaded from initially, unlike in protocols such as HTTP
where the location of the server is what matters. In IPFS, the requested data
can be retrieved from the closest available source, and you can be sure of its
integrity without communicating with the origin of the contents.&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;
&amp;lt;img style=&quot;height:140px;&quot; src=&quot;../ipfs-constellation-1.svg&quot; /&amp;gt;
&amp;lt;img style=&quot;height:140px;&quot; src=&quot;../ipfs-constellation-2.svg&quot; /&amp;gt;
&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;It manages that &lt;em&gt;intergalactic&lt;/em&gt; integrity by addressing data based on its
contents, called the Content ID (or CID, for short). The CID of a piece of data
is derived from that data&apos;s cryptographic hash, so if two users across the
Universe upload the exact same file, it will share the same identifier in the
network.&lt;/p&gt;
&lt;p&gt;Truly decentralized and thus censorship-resistant.&lt;/p&gt;
&lt;h4&gt;IPFS implementations&lt;/h4&gt;
&lt;p&gt;In practice, in order to run your own IPFS node, you use one of the available
implementations of the protocols. The most widely used one is written in Go and
it&apos;s a CLI called &lt;a href=&quot;https://docs.ipfs.io/reference/kubo/cli/&quot;&gt;Kubo&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Content ID specification&lt;/h4&gt;
&lt;p&gt;If the CID is derived from the hash of the data, why not use the raw hash digest
directly as the content identifier? IPFS currently uses the &lt;code&gt;SHA2-256&lt;/code&gt; hashing
algorithm, but the protocol has to be future-proof, and in the future SHA2 might
no longer suffice (I&apos;m looking at you, SHA1).&lt;/p&gt;
&lt;p&gt;The solution: Self-describing hashes (called &lt;em&gt;multihash&lt;/em&gt;), which not only
contain the digest, but also which algorithm generated it and its length:&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;p&gt;multihash = [type][length][value]&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$type$&lt;/td&gt;
&lt;td&gt;Algorithm identifier from the &lt;a href=&quot;https://github.com/multiformats/multicodec/blob/master/table.csv&quot;&gt;multicodec table&lt;/a&gt; (&lt;code&gt;0x12&lt;/code&gt; for &lt;code&gt;SHA2-256&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$length$&lt;/td&gt;
&lt;td&gt;Length of the hash in bytes (&lt;code&gt;0x20&lt;/code&gt; for &lt;code&gt;SHA2-256&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$value$&lt;/td&gt;
&lt;td&gt;The hash digest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In order to represent it in a shorter way, &lt;code&gt;base58btc&lt;/code&gt; encoding was chosen
(which is just &lt;code&gt;base58&lt;/code&gt;, excluding the visually ambiguous characters 0, O, l,
I).&lt;/p&gt;
&lt;p&gt;So, while a &lt;code&gt;SHA2-256&lt;/code&gt; multihash would look like this in hexadecimal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x12204dfa372740668fc766c6d899a1ccdf1f0752f52331a9d9d657c13247bd599b87
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;base58btc&lt;/code&gt; encoded version would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Much cleaner, just 46 characters! You&apos;ll notice that, due to the initial fixed
bytes &lt;code&gt;0x1220&lt;/code&gt;, the &lt;code&gt;base58&lt;/code&gt; version always starts with &lt;code&gt;Qm&lt;/code&gt;, making the IPFS
CIDs very recognizable.&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;p&gt;However, if you&apos;re already familiar with IPFS, you&apos;re probably wondering why
some of your CIDs don&apos;t look anything like that... That&apos;s because what I&apos;ve
explained above is now known as the zeroth version of CIDs, &lt;code&gt;CIDv0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CIDv1&lt;/code&gt; introduces two new prefixes that improve its self-describing capability:
&lt;em&gt;version&lt;/em&gt; and &lt;em&gt;multicodec&lt;/em&gt;. The version is obviously to denote the version of
the CID (&lt;code&gt;CIDv2&lt;/code&gt;, &lt;code&gt;CIDv3&lt;/code&gt;, etc.). The multicodec one denotes the data encoding
method itself (ProtoBuf, JSON, etc.).&lt;/p&gt;
&lt;p&gt;So when dealing with binary we now have:&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;p&gt;\textcolor{red}{[version]}\textcolor{green}{[multicodec]}\textcolor{cyan}{[multihash]}&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;p&gt;But when dealing with human readable formats there should be a quick way to know
which base we&apos;re representing those bytes in...&lt;/p&gt;
&lt;p&gt;Introducing yet another table and prefix, the
&lt;a href=&quot;https://github.com/multiformats/multibase/blob/master/multibase.csv&quot;&gt;&lt;em&gt;multibase&lt;/em&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;p&gt;[base] ;
base(\textcolor{red}{[version]}\textcolor{green}{[multicodec]}\textcolor{cyan}{[multihash]})&lt;/p&gt;
&lt;p&gt;$$$&lt;/p&gt;
&lt;p&gt;A common way to represent &lt;code&gt;CIDv1&lt;/code&gt;s is in &lt;code&gt;base32&lt;/code&gt;, which looks something like
this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;base32&lt;/code&gt; (prefix &lt;code&gt;b&lt;/code&gt;), &lt;code&gt;dag-pb&lt;/code&gt; multicodec and being a &lt;code&gt;SHA2-256&lt;/code&gt;
multihash, we end up with the very recognizable &lt;code&gt;CIDv1&lt;/code&gt; prefix: &lt;code&gt;bafy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;TIP: If you want to easily analyze your CID, you can use the
&lt;a href=&quot;https://cid.ipfs.io&quot;&gt;IPFS CID inspector&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Merkle Directed Acyclic Graphs and IPLD&lt;/h4&gt;
&lt;p&gt;Enough with CIDs, let&apos;s go back to our network...&lt;/p&gt;
&lt;p&gt;We&apos;re not dealing with simple &lt;em&gt;blobs&lt;/em&gt;. It&apos;s a file system, it&apos;s structured and
so we need a way to represent and address the hierarchy of files and folders, in
a way that any change of contents properly propagate upwards, changing all those
hashes/CIDs.&lt;/p&gt;
&lt;p&gt;The solution: &lt;strong&gt;UnixFS Merkle DAG-PB&lt;/strong&gt;! &lt;code&gt;UnixFS&lt;/code&gt; because it&apos;s a Unix File
System. &lt;code&gt;DAG-PB&lt;/code&gt; because it uses that &lt;strong&gt;InterPlanetary Linked Data&lt;/strong&gt;
(&lt;a href=&quot;https://ipld.io&quot;&gt;IPLD&lt;/a&gt;) codec: &lt;code&gt;PB&lt;/code&gt; just means ProtoBuf encoding, but what
about the &lt;code&gt;DAG&lt;/code&gt; part? The &lt;code&gt;Merkle DAG&lt;/code&gt; part...&lt;/p&gt;
&lt;p&gt;The file system can be represented as a mathematical graph:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each file and folder is a node&lt;/li&gt;
&lt;li&gt;Directed graph: There&apos;s containment in the folders, so there are relationships
between the objects (edges) and those relationships have a direction&lt;/li&gt;
&lt;li&gt;Acyclic graph: There are no loops in the graph&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thus it forms a &lt;strong&gt;Directed Acyclic Graph&lt;/strong&gt; (DAG). A Merkle DAG, because we are
generating the CIDs of the contents of each node, including its data and the
CIDs of its child nodes. How cool and efficient is that!?&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;
&amp;lt;img style=&quot;width:330px;&quot; src=&quot;../merkle-dag.webp&quot; /&amp;gt;
&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;Here&apos;s the actual ProtoBuf schema used in &lt;code&gt;DAG-PB&lt;/code&gt; encoding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;message PBLink {
  // binary CID of the target object
  optional bytes Hash = 1;

  // UTF-8 string name
  optional string Name = 2;

  // cumulative size of target object
  optional uint64 Tsize = 3;
}

message PBNode {
  // refs to other objects
  repeated PBLink Links = 2;

  // opaque user data
  optional bytes Data = 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;libp2p: Peer IDs and Keys&lt;/h4&gt;
&lt;p&gt;When in comes to networking, IPFS relies on &lt;a href=&quot;https://libp2p.io&quot;&gt;libp2p&lt;/a&gt;, which
provides several &lt;a href=&quot;https://github.com/libp2p/specs&quot;&gt;specs&lt;/a&gt; and modules useful for
building p2p applications.&lt;/p&gt;
&lt;p&gt;In the IPFS network, we need a way to identify not only the content, but also
the nodes themselves (also called &lt;em&gt;peers&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Not only identify but also, of course, authenticate. For that the libp2p&apos;s
&lt;code&gt;peer-ids&lt;/code&gt; spec is used: Each peer has an associated &lt;code&gt;PeerID&lt;/code&gt; derived from a
cryptographic key pair, which is used for authentication.&lt;/p&gt;
&lt;p&gt;The keys are encoded in ProtoBuf as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syntax = &quot;proto2&quot;;

enum KeyType {
  RSA = 0;
  Ed25519 = 1;
  Secp256k1 = 2;
  ECDSA = 3;
}

message PublicKey {
  required KeyType Type = 1;
  required bytes Data = 2;
}

message PrivateKey {
  required KeyType Type = 1;
  required bytes Data = 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, &lt;code&gt;kubo&lt;/code&gt; uses the &lt;code&gt;Ed25519&lt;/code&gt; key type.&lt;/p&gt;
&lt;p&gt;To generate the PeerID of a node, its public key is encoded using &lt;em&gt;multihash&lt;/em&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the public key bytes length is greater than 42, the key is hashed using the
&lt;code&gt;SHA2-256&lt;/code&gt; multihash method&lt;/li&gt;
&lt;li&gt;Otherwise, the &lt;em&gt;identity&lt;/em&gt; multihash is used (&lt;code&gt;0x00&lt;/code&gt; prefix, as per the
multicodec table), which just means that the raw bytes are used directly as
the hash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Regarding human-readable formats, they&apos;re treated the same way as the &lt;code&gt;CIDv0&lt;/code&gt;
and &lt;code&gt;CIDv1&lt;/code&gt;, i.e. They are encoded using &lt;code&gt;base58btc&lt;/code&gt; or &lt;code&gt;multibase&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;libp2p: Kademlia Distributed Hash Tables&lt;/h4&gt;
&lt;p&gt;Now that we know how data is being stored and addressed, it&apos;s time to make it
available to the network, but how does any node know where it is?&lt;/p&gt;
&lt;p&gt;Imagine if a node requested a block and had to traverse the network, node by
node, blindly until it found it, how slow that would be... We obviously can&apos;t
have a centralized indexer either, and we can&apos;t store the whole global state of
&lt;em&gt;which node has which data&lt;/em&gt; in each node...&lt;/p&gt;
&lt;p&gt;The goal is for each node to contain a manageable amount of information about
the data location state. So the state must be equally distributed between all
the nodes in the network, and that must ensure reliability and speed (i.e.
communicating with the smallest number of nodes) when retrieving content.&lt;/p&gt;
&lt;p&gt;It surely sounds like an impossible task, but it has a genius solution:
&lt;strong&gt;Kademlia Distributed Hash Tables&lt;/strong&gt; (DHT).&lt;/p&gt;
&lt;p&gt;&amp;lt;br /&amp;gt;&lt;/p&gt;
&lt;p&gt;We start by mapping the PeerIDs into 256bit numbers so that we know all peers
fit in a defined finite space, called &lt;em&gt;address space&lt;/em&gt;, i.e. mapped between &lt;code&gt;0&lt;/code&gt;
and &lt;code&gt;2^256-1&lt;/code&gt;: To achieve that, we just do &lt;code&gt;SHA2-256(PeerID)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My brain can&apos;t operate with 256bit numbers yet, so in the example bellow, let&apos;s
assume our network is super small and can fit it in 3 bit numbers instead.&lt;/p&gt;
&lt;p&gt;So we&apos;re peer &lt;code&gt;010&lt;/code&gt; and the other peers in the network are &lt;code&gt;011&lt;/code&gt;, &lt;code&gt;101&lt;/code&gt;, &lt;code&gt;110&lt;/code&gt;
and &lt;code&gt;111&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 0   1   2   3   4   5   6   7
-------------------------------
000 001 010 011 100 101 110 111
-------------------------------
         O   X       X   X   X
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We&apos;ll use the bitwise XOR as a way to measure &lt;em&gt;distance&lt;/em&gt; between the numbers,
i.e. The &lt;em&gt;distance&lt;/em&gt; between $a$ and $b$ is $a \oplus b$, and we&apos;ll create a
binary tree for each node based on the possible distances to it: Each level is 1
bit of distance and we populate it with the nodes accordingly (notice how our
node is at distance &lt;code&gt;000&lt;/code&gt; from itself, since $a \oplus a = 0$, which in our tree
looks like &lt;em&gt;left, left, left&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;We&apos;ll then branch off of the tree from our node at each level, creating groups
(called &lt;em&gt;buckets&lt;/em&gt;), and we&apos;ll pick a number $k$, and we store a maximum of $k$
nodes per bucket.&lt;/p&gt;
&lt;p&gt;These are called &lt;em&gt;k-buckets&lt;/em&gt;, denoted in green in the figure bellow.&lt;/p&gt;
&lt;p&gt;$k = 2$
&amp;lt;img src=&quot;../routing-table-example.webp&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;first&lt;/em&gt; amazing part is that if each node stores a list like that, then we
can reach any part of the network in just $\mathcal{O}(\ln{n})$ hops (where $n$
is how many nodes are in the network).&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;second&lt;/em&gt; amazing part is that we take advantage of this setup by also
mapping the CID into the address space, &lt;code&gt;SHA2-256(CID)&lt;/code&gt;, and store the record
that our node can provide that content in the DHT of the closest nodes to that
CID, i.e. The peers where &lt;code&gt;SHA2-256(PeerID)&lt;/code&gt; $\oplus$ &lt;code&gt;SHA2-256(CID)&lt;/code&gt; is the
lowest.&lt;/p&gt;
&lt;p&gt;Let&apos;s say our network has 1000000 nodes, we would be able to reach &lt;em&gt;any&lt;/em&gt; content
from &lt;em&gt;any&lt;/em&gt; node in around 20 hops!&lt;/p&gt;
&lt;p&gt;IPFS&apos; DHT uses 256bit address space and $k = 20$.&lt;/p&gt;
&lt;h4&gt;Garbage collection and content pinning&lt;/h4&gt;
&lt;p&gt;There are only so many nodes in the network, and there&apos;s much more content being
uploaded all the time. Nodes store the contents of other nodes, contributing to
the availability of those contents, but storage is finite and with time, a node
gets filled and must clear old content in order to be able to provide new one
(this process is called &lt;em&gt;garbage collection&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;A node may choose to prioritize certain content and not garbage collect it over
time, &lt;em&gt;pinning&lt;/em&gt; it, guaranteeing availability of that content indefinitely.&lt;/p&gt;
&lt;p&gt;Services running several nodes can pin your data for a fee, those are called
pinning services.&lt;/p&gt;
&lt;h4&gt;Gateways&lt;/h4&gt;
&lt;p&gt;A link to an IPFS block looks like this
&lt;code&gt;ipfs://{CID}/{optional-path-to-content}&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ipfs://QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz/welcome-to-IPFS.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But most of our browsers don&apos;t support the IPFS protocol (&lt;code&gt;ipfs://&lt;/code&gt; links): For
that, IPFS gateways exist, which are HTTP APIs that interact with a node for
returning resources. All IPFS node implementations provide a gateway.&lt;/p&gt;
&lt;p&gt;Example of a gateway URL:&lt;/p&gt;
&lt;p&gt;https://ipfs.io/ipfs/QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz/&lt;/p&gt;
&lt;h4&gt;Content immutability and IPNS&lt;/h4&gt;
&lt;p&gt;One limitation of the network we&apos;ve been describing is that since the data is
addressed based on its contents, everything is immutable. If you share a link,
it will forever point to the same data. The solution: &lt;strong&gt;InterPlanetary Name
System&lt;/strong&gt; (IPNS).&lt;/p&gt;
&lt;p&gt;If we want to achieve mutability, the simplest way is by using a pointer between
some identifier and a CID, and then we update that pointer with a new CID
everytime we want to point to new data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[fixed ID] -&amp;gt; [CID]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s precisely what happens in IPFS: Another record stored in the
previously described IPFS DHT is called the IPNS record, which maps the PeerID
(or the ID of another key the peer has access to, called &lt;em&gt;name&lt;/em&gt; in this context)
to an arbitrary identifier (which actually doesn&apos;t have to be a CID, it can also
be another IPNS name).&lt;/p&gt;
&lt;p&gt;IPNS records are short lived, and since they must be cryptographycally signed
there is no &lt;em&gt;pinning&lt;/em&gt; in IPNS, the solution is to republish the record
periodically.&lt;/p&gt;
&lt;p&gt;IPNS records can be accessed through gateways: Just replace &lt;code&gt;ipfs/&lt;/code&gt; with
&lt;code&gt;ipns/&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Hosting on IPFS&lt;/h4&gt;
&lt;p&gt;Finally, we&apos;ve covered most of the building blocks and features of IPFS,
congrats if you have made it this far!&lt;/p&gt;
&lt;p&gt;I&apos;ll conclude by outlining some of the challenges I have faced and solutions I
came up with in the deployment of this blog in hopes of making the process
easier for someone else: Perhaps I&apos;ve already convinced &lt;em&gt;you&lt;/em&gt; to host your site
on IPFS :).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;For now, I don&apos;t want to run a node 24/7, so I&apos;m using a
&lt;a href=&quot;https://pinata.cloud/&quot;&gt;pinning service&lt;/a&gt; (one day I&apos;ll probably get a
Raspberry Pi and run it myself, actually), which I call in a GitHub workflow
in order to achieve continuous deployment. It automatically commits the new
CID to a &lt;a href=&quot;https://github.com/ndavd/blog/blob/main/cid.txt&quot;&gt;file&lt;/a&gt; in the
repository&lt;/li&gt;
&lt;li&gt;Different IPFS gateways have different domains and routes, so all links in
the website must be relative, e.g. &lt;code&gt;../my-image.webp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I am using my ENS domain (topic for another time) as a decentralized way to
access the blog: Its &lt;code&gt;contenthash&lt;/code&gt; field supports IPFS, but each on-chain
update is a transaction which costs money, and no one wants to pay for gas
each time one changes a blog... Fortunately, we have already found how
mutability can be achieved in IPFS: We use an IPNS name&lt;/li&gt;
&lt;li&gt;IPNS needs to be updated regularly (expires after 24 hours, by default), so I
setup a systemd timer that calls a shell script which updates the IPNS record
with the latest CID&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The link to the source code of this blog can be found in the footer.&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- $ ipfs cid format -v 1 -b base32 QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz --&amp;gt;
&amp;lt;!-- bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4 --&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- $ ipfs cid format -f &quot;%M&quot; -b base58btc bafybeicn7i3soqdgr7dwnrwytgq4zxy7a5jpkizrvhm5mv6bgjd32wm3q4 --&amp;gt;
&amp;lt;!-- QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz --&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- $ ipfs cid format -f &quot;%M&quot; -b=base16 QmTb3PouhpDfE4zSRXhW4tBW6z47kbisZ9cNjNPDZ4jazz --&amp;gt;
&amp;lt;!-- 12204dfa372740668fc766c6d899a1ccdf1f0752f52331a9d9d657c13247bd599b87 --&amp;gt;&lt;/p&gt;
</content:encoded></item></channel></rss>