Peer-to-Peer Web Applications v4

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 133

MEAP Edition

Manning Early Access Program

Peer-to-Peer Web Applications


Version 4

Copyright 2023 Manning Publications

For more information on this and other Manning titles go to manning.com.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


Welcome
Thank you for purchasing the MEAP for Peer-to-Peer Web Applications. This book is
written for web developers that are familiar with Javascript and want to transition those
skills to build applications for the next evolution of the internet we know as Web3. To get
the most from this book, readers should have familiarity with writing and deploying Web2
applications to cloud providers such as Amazon Web Services, as well as basic
knowledge of blockchain technology and smart contracts.
When I first started building for Web3 a number of years ago, an overwhelming
majority of resources I found, focused on Web3 in the context of blockchains and smart
contracts. These resources often placed the blockchain and smart contract first, with
programming for the rest of the application being secondary to whichever features the
smart contract could support. This book reverses that order and designates blockchain
as optional in Web3, explaining Web3 application architecture as peer-to-peer first, with
blockchain being used only where it fits. It is my hope that reversing this order will
greatly expand the readers' perspective for the types of applications that are possible in
Web3, as well as demonstrate, end-to-end, why Web3 represents an evolution for the
whole of the internet, not just finance.
This book divides Web3 application architecture into 6 application layers which
include: networking, compute, storage, authentication, payments, and user interface. By
the end of the book, the reader will have completed the journey of building an example
of each application layer to create “Code Radio”, a Web3 streaming music application
that runs fully client-side and peer-to-peer. This application is built first without
blockchain, before being updated to make use of blockchain-specific benefits.
I hope you find this book's view of Web3 to be both new and full of possibilities.
Please do let me know if you have any questions or general feedback as you read the
MEAP edition of this book. I’m just a few clicks away, in the liveBook discussion forum.
Welcome to Web3!
—Steven Platt, PhD

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


brief contents
CHAPTERS

1 Defining Web3
2 Using blockchain when data must be verified

PART 1: BUILDING FOR WEB3 WITHOUT BLOCKCHAIN

3 Designing applications that run client-side


4 Using Javascript for client-side storage

PART 2: CONNECTING APPLICATIONS PEER-TO-PEER

5 Using Libp2p to connect applications peer-to-peer


6 Using Libp2p for content distribution

PART 3: USING CRYPTOGRAPHY FOR AUTHENTICATION AND DATA


OWNERSHIP

7 Peer-to-peer identity and ownership using public-key cryptography


8 Verifying general purpose data using zero knowledge cryptography

PART 4: ADDING BLOCKCHAIN AS A NEUTRAL THIRD-PARTY

9 Adding payments to Code Radio using Ethereum


10 Adding payments to Code Radio using Solana
11 Adding payments to Code Radio using Mina

PART 5: LAUNCHING YOUR WEB3 APPLICATION

12 Packaging and distributing your Web3 application


13 Why Web3 applications fail

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


Appendix A. Downloading and running the Code Radio application
Appendix B. Encoding music files for IndexedDB storage

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


1

1
Defining Web3

This chapter covers


Defining Web3
Motivations for migrating systems to Web3 architecture
Detailing how blockchains function underneath
Identifying the role of blockchain within a broader Web3 ecosystem

Web3 is not just blockchain or cryptocurrency. Web3 is social media, streaming videos,
games and every other application that runs over the internet today; but with application
architecture that makes every participant an owner of the system.
As with any generational change in technology, during early phases when you are
unsure what a technology will ultimately become—and before the infrastructure to
support it has matured—the first introduction to such generational change is just
marketing: a promise, a projection, an announcement with a list of all the problems to
be solved …soon. Getting from point A to point B in this context is naturally confusing,
perhaps a bit frustrating, and businesses, a potential risk.
Now imagine in the previous scenario that the infrastructure you are waiting for is
proposed as a replacement for the whole of the internet. Where to begin? What does that
even mean? The good news is that the internet is not being replaced. It turns out that
the internet infrastructure that you have is already pretty great and continues to evolve.
Even better, you have seen this transition before and have practiced it, because you are
already at Web2!

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


2

The conclusion of that original transition, which feels so natural today, is that the
original internet underpinnings were brought along for the ride; we still had use for them.
The static HTML pages of the Web1 era over time were enhanced with Javascript-
powered interactivity, user-generated content, and an ad-supported economic model
which all used the humble HTML page as a base. Web1 didn’t cease to exist as the world
ran full speed into Web2, it came along for the ride. Attempting to pick a date, company,
or technology that marked the end of Web1 and beginning of Web2 misses the point. It’s
at the point that the structural capabilities of the web ecosystem evolve to such an
extent that it changes our expectation of what the internet could and should be that
marks generational change. You are undergoing this next shift in collective expectations,
today you are on our way in the transition to Web3.
This book is intended to be an on-ramp, a holistic view of Web3 application
architecture and what it means for building the applications that take advantage of the
enhancements that Web3 brings; both with, and without blockchain. To do this,
throughput this book, application architecture is presented as a series of layers as shown
in figure 1.1.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


3

Figure 1.1. When building Web3 applications, you are able to customize how you build each layer, based
on your application’s functionality and technical needs. Each of these layers is explained in detail with
dedicated chapters in this book.

After learning what defines Web3, you will practice building Web3 applications layer-by-
layer as you progress through this book.

1.1 What is Web3 and how is it different?


In a manner similar to how cloud computing allowed the rapid deployment of systems
around the world, Web3 takes this a step further to allow expansion of systems without
direct capital investment. In this current evolution of the web, it is the backing
infrastructure itself that becomes user-generated.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


4

A user installs, operates, or engages with a Web3 system to the extent that they find
worthwhile utility in that system. Rather than a facility to exchange cryptocurrency, Web3
should be thought of in a more agnostic context as an exchange of utility , which may or
may not include direct financial payment. A Web2 example of this is torrent file sharing.
Such torrent systems function by accessing files in multiple parts that are downloaded in
parallel. This parallel download means that when operating a torrent client, a user
receives the utility of faster downloads. In exchange, the torrent application being used
may also support the re-transmission of that content, allowing the user the choice of
providing that same utility to others. When agreeing to retransmit downloaded content, a
portion of that systems infrastructure becomes user-generated without direct payment
being made. Web3 technologies such as blockchain build on top of this, mechanics that
enable identity, entitlements, and payments in peer-to-peer formats that were not
previously possible, and in doing so, carry forward the already existing peer-to-peer
architecture of Web2, making it inseparable and core to Web3.
For the purposes of this book, I define Web3 as the following: An evolution of the
existing internet in which systems support verifiable self-sovereign identity in addition to
operating peer-to-peer (figure 1.2).

Figure 1.2. Web3 is inclusive of applications, services, infrastructures, and further mechanics that are
internet connected; so long as they support verifiable self-sovereign identity and are peer-to-peer in
nature.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


5

The wording in this definition is intentional, both for what it includes and what it leaves
out. Let us take a closer look at its component parts to understand why this definition is
required and how it is different from Web2 designs.

1.1.1 Verifiable Self-Sovereign Identity


Self-sovereign identity specifies that individual users have control and ownership over
their identity within Web3 systems. This means that no singular entity outside of a user
has the ability to invalidate, restrict, or otherwise control the participation and
entitlements tied to an identity in Web3 systems. An important modifier that is included
here is the distinction of being verifiable . If you were wondering where blockchain fits
into Web3, it is here. Without a central party or company providing guarantees, an
additional mechanism is needed to provide this. Recording an identity and its associated
activity and entitlements in a decentralized (peer-to-peer) permanent record is one way
to enable self-sovereign identity that is also verifiable. Finally, a user is also able to
choose no identity or to be anonymous in Web3, which is considered self-sovereign as
well. For applications that do not require an identity to participate, blockchain may not be
required to build a system fitting the definition of Web3.

NOTE For more information about self-sovereign identity, seeSelf-Sovereign Identity


Decentralized digital identity and verifiable credentials (https://www.​m anning.com/​books/self-​
sovereign-​identity)

1.1.2 Peer-to-peer
Peer-to-peer as a paradigm has existed for a significant portion of Web2’s development,
and here it is not the new addition. What has changed in Web3 context is that peer-to-
peer is the default mode of operation (figures 1.3 and 1.4). Defining Web3 using the term
peer-to-peer, rather than decentralized, also solves a few semantic problems. First it is
clear about the technologies and methods preexisting in Web2 that are being reused
(peer-to-peer architecture). The second is that it allows the definition to be as close as
possible to a binary state and nullifies any debate over a threshold for when when a
system can be considered decentralized. A system operates peer-to-peer or it does not.
The Bitcoin blockchain and Bittorrent are examples of systems that operate peer-to-peer.
Another example that you may be actively using today is the Windows Update function of
the Microsoft Windows 10 and later operating systems. Starting with Windows 10,
Microsoft has introduced an option named "Delivery Optimization" which allows Windows
Update file downloads to be completed peer-to-peer, in-part to reduce network utilization
during updates within large campus and enterprise environments. Throughout this book,
the term peer-to-peer is used in place of any use of the word "decentralized".

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


6

NOTE For more information about the Delivery Optimization feature of Microsoft Windows, see
What is Delivery Optimization? (http://mng.​bz/d1Gw)

Figure 1.3. Centralized systems have a central point of control.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


7

Figure 1.4. Peer-to-peer systems allow participants to connect to each other without a central point of
control. Web3 applications, as a default, exchange information peer-to-peer.

1.2 What Web3 is not …yet


Now that you know what Web3 is, it is also important to understand a few critical items
that are often associated with Web3, but are not always the default state. Two of these
items are data ownership and data privacy. These two items can be viewed as macro
trends, or areas of active development, where Web3 systems are expected to see
further advancements in coming years. Because these are not ecosystem defaults, when
designing your Web3 application, you will want to give explicit consideration to whether
they will be a feature of your system and how you would like to implement them.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


8

1.2.1 Data ownership


Many see Web3 as a response to undesired use and abuse of user data. Contrary to this,
applications that run peer-to-peer often store data on any number of devices that are
beyond user control. In practice, once data is made public and shared peer-to-peer,
there are no default controls to later update or remove this data from circulation. It may
not even be possible to locate or know of every copy of that data that exists unless a
Web3 application has been designed explicitly to support data ownership.

1.2.2 Data privacy


A more specific sub-component of data ownership is data privacy. In the first iterations
of Web3, systems such as Bitcoin; transaction data and the amount of Bitcoin in each
wallet are public data. Further, systems that interact with Bitcoin, Ethereum, and similar
public blockchains also store that portion of their data publicly unless specific provisions
are made in the application architecture to make that data private. At the time of writing,
a new category of Web3 systems such as the Mina and Aleo blockchains rely on zero
knowledge cryptography as a means to store data privately while keeping it verifiable.
Zero knowledge cryptography is a form of cryptography that allows participants to prove
that a piece of information is true, without having to disclose the data itself. Some
examples of this include a participant proving that they are permitted to access a system
without revealing their personal password; a participant proving that they have
purchased an item without revealing the price or who it was purchased from; or an event
participant proving they meet a specific age requirements, without sharing their age.
Sharing and verifying data in this way is also referred to as zero knowledge proof.

NOTE For more information about zero knowledge cryptography and zero knowledge proofs, see
Real-World Cryptography (https://www.​m anning.com/​books/real-​world-cryptography)

1.3 The Fediverse vs Web3


An additional area that has increased in awareness and is often mentioned in
conversations regarding Web3, is the fediverse . The term fediverse is a concatenation of
the words federated and universe, and is most commonly applied to social media
applications that operate on top of a shared protocol that allow them to communicate
with each other peer-to-peer. Although fediverse applications communicate peer-to-peer,
whether a fediverse application is considered Web3 will depend on the individual
application. To demonstrate this, the following example compares the applications
Mastodon and PeerTube.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


9

Mastodon is a social messaging application that allows users to create accounts and
post messages both publicly and privately, similar to Twitter. The Mastodon application is
open source and is run on servers operated by the community. Mastodon users can
create an account on one server, while following and messaging accounts from other
servers. If a user does not like the experience of any single Mastodon server, such as if
a server is slow; the user can export their account and import it to a different Mastodon
server. The user can even launch their own Mastodon server to import their account
data, allowing the identity to be self-sovereign. A more recent feature of Mastodon is the
ability to verify user identity. Rather than rely on a trusted third party for verification,
Mastodon allows users to add a line of code to a personal website which serves as a
secondary verification of identity. As long as the Mastodon server can read the
verification code, a green check mark is placed alongside the website link on the user’s
profile. Following the definition presented in this chapter, Mastodon is part of Web3.
PeerTube is another fediverse application that allows users to create accounts on
community-operated servers that are connected peer-to-peer. Users of the PeerTube
application can upload videos to personal channels and follow the channels of others,
similar to YouTube. Unlike Mastodon, at the time of writing, PeerTube does not allow
users to export their accounts to move themselves to another PeerTube server. Because
identities created on PeerTube are not self-sovereign, the application is not considered
Web3 (figure 1.5).

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


10

Figure 1.5. Although fediverse applications operate peer-to-peer, only fediverse applications supporting
self-sovereign identity can be considered Web3.

1.4 Should my application move to Web3?


Although this book targets entirely a Web3 architecture, as you move further into the
design of Web3 applications, it will become apparent that a single application can be
updated to include infrastructures from both Web2 and Web3. Depending on the
application usage and business model, it is expected that there are cases where Web2
architecture may remain preferred, even after Web3 has matured. Web3 and Web2 are
not mutually exclusive.
One area where a Web2 architecture is very successful, are cases where absolute
control of a system is required. When adopting Web3 architecture and operating a
systems infrastructure on a peer-to-peer basis, there is reduced control to specify who
and how that infrastructure is delivered. Web3 architecture functions similar to today’s
open source models in this way. Control is not absolute, instead Web3 architects function
as stewards of Web3 systems, providing leadership and support in a codependent
exchange with the wider community of system participants.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


11

Regardless of the specific application, most any system can be architected entirely on
a Web3 architecture. When defining the system architecture, whether for Web2 or Web3,
you must compare tradeoffs among different alternatives. If you are already operating a
Web2 application or service, some items to consider that could help in making the
decision of whether it is time to migrate your system to Web3 architecture are:

1.4.1 What level of performance is required?


A major part of application architecture is choosing infrastructures that can physically
support the level of usage expected, while remaining responsive. When investigating
Web3 replacements for your existing infrastructure, be sure to consider how these new
infrastructures perform and how they will change the overall performance of your
application. A Web3 infrastructure may become extremely popular, but still remain
incompatible with the levels of performance your application requires.

1.4.2 Could your application benefit by going open source?


The experience and considerations of moving to Web3 mirror quite closely the
experience and considerations on whether to contribute or use open source software. For
example, a system or service that is offered for free, can become very difficult to
support if its popularity becomes orders of magnitude greater. Moving such an
application to a Web3 architecture could allow offloading the operation of the underlying
infrastructure by making it run peer-to-peer, while also allowing others to contribute to
your project. If your application could benefit from being made open source, it may also
be a good candidate for an architecture that is Web3.

1.4.3 What problems are solved?


This last item is the most generic. With all things considered, do the benefits of moving
your application to a Web3 architecture outweigh the costs? Are there specific problems
that are being solved, such as concerns of user-ownership, privacy, censorship, or
centralization? Web3 architecture is not a one-size-fits-all solution. If Web3 does not
solve a specific problem for you and your applications users, it is perfectly ok to reach a
conclusion that Web2 remains the correct choice.
If your application serves a user base that continues to show strong demand growth,
a Web3 alternative may not prove as popular. In a shorter term, ease of use will
continue to be a large factor in user adoption. Can you redesign your application atop
Web3 while retaining an equivalent ease of use? Better yet, can you offer the same
system or service atop Web3, without users noticing a change in usability at all? If you
can answer “yes” to the latter question, your application should be well positioned as
user preferences shift between Web2 and Web3 for various applications.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


12

1.5 Web3 is more than Blockchain


For many people, blockchain is the first item that comes to mind when thinking of Web3.
At this time, blockchain mechanics and usage may indeed be the most well understood
portion of Web3 - but Web3 is so much more than blockchain. In Web2 architecture, we
wouldn’t ask our database to also handle compute, or use our authentication provider for
streaming media playback. As raw infrastructure, If you think of blockchain as a method
of storage or replacement for a database, it becomes more intuitive to view it as part of
a larger application stack that will be required to deliver the most feature rich
application. This book will present Web3 application architecture in this much wider
context, but for a deep dive into blockchain mechanics and smart contract development,
books such Blockchain in Action and Building Ethereum DApps from Manning Publishing
give specific and expanded focus to blockchain.
Even with an expanded focus, blockchain technology remains a pillar for enabling a
number of Web3 use cases. This section provides a review and context for the role that
blockchain technologies plays when combined with non-blockchain Web3 mechanics.

1.5.1 Understanding the more narrow role of Blockchain in Web3


Blockchain is often thought of as enabling a trustless web. The term trustless is a bit of a
misnomer. In the context of blockchain, it means that a system can operate without a
central authority or trusted intermediary, therefore the system is trustless.
Prior to blockchain technology, many of the peer-to-peer systems that are covered in
this book were in active use. A limitation that existed however, was a general ability to
know that these systems were safe to use, even as you had no control over where data
came from or who you were connecting to. An experience that many have had with
peer-to-peer applications would be the example of downloading a file or attachment,
which turned out to be infected with a virus. When this happened, there was no one held
accountable for causing the harm (no provable ownership), there may be no one to
report the incident to, and critically, not many options to prevent it from happening again
in the future (no reputation). Due to this level of risk previously involved in interacting
peer-to-peer, an overwhelming majority of users chose to continue using centralized
systems with a formal trusted party at the center, even though systems that removed
the central authority were possible.
Within Web3, blockchain serves the role of providing a solution to these specific
limitations previously mentioned, while not directly replacing the other peer-to-peer
mechanics that existed in Web2 . Web3 use cases where blockchain is often required, are
with use cases which require provable ownership or reputation. From these two areas,
you get the three most common applications of blockchain technology currently within
Web3, which are currency payments, financial contracts, and identity and access
management. In the chapter that follows, the blockchain use case is expanded and
combined with additional data to help you choose a blockchain to use with your Web3
application …if required.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


13

1.6 Summary
Web3 is defined as: An evolution of our existing internet in which
systems are peer-to-peer and support verifiable self-sovereign identity.
Choosing to be anonymous is a form of self-sovereign identity. Web3
does not include data ownership or data privacy by default …yet.
Web3 is an exchange of control, but also an exchange of utility among
a community of participants.
Application and infrastructure code are no longer separate. Controls for
storage, compute, and routing are now directly included in Web3
application code.
Web3 and Web2 are not mutually exclusive. Use cases will continue to
exist where a central authority is preferred or mandated through
regulation.

Whether you should build your application for Web3 will depend
on your individual use case. Some common considerations are:
Are you already a user or supporter of open source software?
Do you want to operate a business based on your software?
What model do your users prefer?
What levels of performance are required?
What existing problems do you have that a Web3 design will
solve?
Not all Web3 applications require a blockchain. In Web3, blockchain can
be used to solve the more narrow problem of data verifiability and
trust.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


14

2
Using blockchain when data
must be verified

This chapter covers


Comparison of application architectures between Web2 applications which are
centrally managed and Web3 applications which operate peer-to-peer.
Mechanics for distributing, managing, and upgrading Web3 applications.
An overview of the blockchain landscape and newer zero-knowledge technologies.

After reading Chapter 1, you now know the concrete definition of Web3 and an
introduction to how it is changing application architecture. Although Web3 is much larger
than just blockchain; blockchain still plays a large role in Web3, by making mechanics
such as payments, able to operate peer-to-peer. In this chapter, you will get an extended
explanation of all things blockchain. How they function underneath, what makes them
secure, their performance, as well as direct comparisons of the now extensive range of
blockchain formats and features. By the end of this chapter, you will have a solid
foundation to investigate and choose blockchains to use in your next Web3 project …if
required.

2.1 How Blockchains function underneath


By pulling back the curtain on how a blockchain functions underneath, you can begin to
understand how a blockchain can support an application operating without a central
trusted party. Perhaps the most simple explanation of blockchain would be to define it as
a chain of blocks that handle the storage of a system’s state.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


15

2.1.1 What blocks?


Data blocks. This is why blockchain is often referred to as a database. Anything that can
be stored in a database has the potential to be stored in a blockchain block. The
structure of how a blockchain stores that data is what separates it from a traditional
database.
Although the specifics change between blockchain projects. When data is stored in a
blockchain block, it includes a set of standard information, including the sender and the
receiver. If a blockchain supports payments, then a payment amount and/or wallet
balance may be included in the data block. Changing the format and contents of what is
stored within a block is the primary way blockchain platforms enable new features. The
content stored within the block is also what makes them tamper-resistant.

2.1.2 Benefiting from tamper resistance


The first block of a blockchain is referred to as the genesis block and has no records
preceding it. From that first block, when the next block is added, the first block gets
encoded into a representation known as a hash. In computer science, a hash is created
using a mathematical function that takes an arbitrary input and produces an output of a
fixed size. The hash is important because it is globally unique and is considered a
fingerprint or verification of the original data. If that same information from the genesis
block is hashed again, it would produce the same hash output. If the original data is in
any way modified, hashing that data will produce a different hash value. Because of this,
it is possible to know that data has not been changed - because you can hash the
original data yourself to confirm the data is not changed, removing the need to trust the
person who sent it.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


16

Figure 2.1. In a blockchain, data stored within a data block is hashed, and the resulting hash is placed
into the next block that is added. When data is chained together in this way, a record stored in a
blockchain block cannot be changed later, unless a majority of participants accept the change and agree
to update all the blocks that were added after to include a changed summary hash. Because of this
required agreement or consensus, records stored in a blockchain are considered tamper-resistant, even
though they have no central owner.

2.1.3 Building a chain of blocks


With a single block of data, it is possible to change both the original data and the hash to
have them both match. In a blockchain system to prevent this, after the initial genesis
block is hashed, that hash is included with the data of the next block. This means that
every new block holds a representation of the data of the block before it; chaining them
together. Because of the way the hash works, if data is changed within any block, the
hash that was included in the block after must also be changed, and the block after it,
until the entire chain is rewritten.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


17

Figure 2.2. If a block within a blockchain system is ever modified, it could be confirmed as invalid due to
its hash no longer matching the hash which was recorded into blocks that were created after.

2.1.4 Consensus, the engine that powers blockchain


At the heart of any blockchain system, is the algorithm that allows all participants to
agree on the current state. There are a large variety of algorithms that can handle this
task and the algorithm chosen for consensus largely dictates the overall performance,
scaling, and security model of a blockchain platform as a whole. If your Web3 application
requires identity, payments, or other functions that are best handled by a blockchain,
then understanding different consensus models may be the most important factor in
determining if a specific blockchain can support the performance, capacity, or security
model required from your design.
You can take two high-level examples to see how this functions in practice using
Proof-of-Work (PoW) and Byzantine Fault Tolerance (BFT) consensus algorithms.
With PoW, updates are broadcast to all participants. From this point, participants race
to produce a hash value that meets a specific length value set by the network. Once a
suitable hash is produced by a participant, it is then broadcast throughout the network
and accepted as the updated state. The hashing work done by all other participants at
this point is discarded. If this sounds inefficient to you, you would be correct. It is
important to note however that the PoW system, in this case, was designed to place a
high priority on security, while still functioning as an open system (often referred to as
permissionless). Because the participant who creates the valid hash is random, and that
hash is sent to the whole network; if an attacker wanted to manipulate updates to the
system, they would need to control a majority of the computing power in the system to
produce a manipulated hash value and still have the network accept the update.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


18

NOTE In a PoW network, it is also possible that two participants produce an acceptable hash
value at the same moment; in this scenario, a fork occurs in which participants accept and begin
building on the valid hash and block that was received first. The most common way this conflict is
resolved is using a "longest chain rule". Additional information about PoW conflict resolution and
the longest chain rule can be found within the original Bitcoin whitepaper: https://bitcoin.​
org/en/bitcoin-​paper.

One of many alternatives to PoW is BFT. Unlike PoW, which is designed entirely for open
participation, BFT consensus mechanisms are commonly configured to accept updates
with a lower amount of participant agreement; 33% for example in place of the 51%
threshold used in Bitcoin networks' PoW consensus. This change allows blockchain using
BFT consensus to reach an agreement and therefore apply updates faster. This increase
in performance is significant in that it unlocks the ability to use blockchain with
applications that require more frequent updates, or high transaction rates. The tradeoff
that is made in the case of BFT, is that overall security is reduced. If agreement can be
reached with only 33% of participants, then an attacker would only need to have control
of 34% of participants in the system to control which updates are accepted. This format
of higher transaction capacity and reduced security makes BFT-based blockchain
systems popular in private or consortium deployment, where there is still no central
owner, but participation is controlled or otherwise limited to known parties.

2.2 Picking a blockchain for your project, if required


In peer-to-peer systems, the responsibility to verify information falls to the individual
participants. In practice, this is difficult, since in many cases, two parties can both have
identical data and still form a different understanding of what is true.

Using the weather as an example, given a temperature of 65o degrees Fahrenheit


(18.3o C), a person from a hot weather climate may understand this to be cold weather,
while another person from a cold weather climate understands it to be hot. Imagine now,
that when the weather is cold each person is supposed to make a payment for heating.
In this example, only one of the two participants makes a payment. Which participant is
following the rules in this case?
As an application developer, you may look at this example and solve the confusion by
creating a single set of rules and putting those rules into an application that both
participants use. This program defines temperatures at or above 60o Fahrenheit (15.5o
C) as hot, and temperatures below this as cold. Both participants now use this application
and the confusion appears resolved. Everyone has the same set of rules, and the
application now also reports for both participants, that all required payments have been
made.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


19

Going forward in time, one participant realizes that they can edit the rules of the
application so that they never have to pay for heat. Their version of the application is
now set so that payment for heat is due when the temperature is below -32o Fahrenheit
(-35.5o C) so that a payment is never due. Both participant’s applications, in this case,
continue to report that all required payments have been made.
This previous example demonstrates one of the largest limitations of systems that
operate peer-to-peer, which is a lack of authority or ability to enforce rules. In systems
where there is a natural incentive to collaborate, combined with little or no upside for
misbehaving, the need for rule enforcement can be removed or greatly reduced. The
streaming radio application you will build in the next chapter is an example of this. Its
application functions are limited to playing, pausing, and changing between predefined
stations. There is very little to be gained by modifying the application to make it appear
paused when it is playing. If your Web3 application can be damaged or exploited by
misbehavior, using a blockchain to make records verifiable is the primary way issues of
collaboration are resolved in Web3. Some Web3 application examples where blockchain
can be used for verifiable records include:
Voting systems
Identity and reputation systems
Financial systems
Supply chain and logistics systems
Legal and contract systems

These categories of applications are not intended to be exhaustive, and within each of
them are several sub-classes of application. Once you have chosen a Web3 application
idea and have identified that portions of its application data must be verifiable, the next
step in defining the architecture of your application is identifying the blockchain and
blockchain ecosystem you will use. The following section provides a menu of sorts, for
the ecosystem of blockchain technologies available in Web3.

2.2.1 Layer 1 blockchains: currency, contracts, and global state


Blockchains are often described in terms of layers. Layer 1 blockchains are blockchains
that can be used in isolation. These are blockchains such as Bitcoin, Ethereum, and Mina,
which have abilities to handle network consensus and currency transactions while
remaining secure and fault-tolerant.
Blockchains that are designed to have great security and fault tolerance typically
make tradeoffs for performance in other areas. In the case of the three projects just
mentioned, one area in which they all make a performance trade-off is in transaction
processing time. At the time of writing, Bitcoin is capable of handling roughly 7
transactions per second, while Ethereum can handle around 30 transactions per second
(TPS), and Mina roughly 22. While not exhaustive, figure 2.3 shows a graph of 10
popular layer 1 blockchains and their transaction throughput at the time of writing.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


20

Figure 2.3. Comparison of popular layer 1 blockchains, based on reported peak TPS at the time of writing.

CURRENCY SUPPORT
Most all layer 1 blockchains incorporate a native digital currency and impose a fee for all
activities occurring on the chain (one exception being the Hyperledger project). If you
are building a system that will charge users for access, you will need to consider
currency impacts including liquidity, price stability, and transaction costs. A currency that
fluctuates significantly in price or carries high transaction costs, can impact users'
ongoing ability to interact with your Web3 application.
Today a category of currencies labeled stablecoins has been introduced in part to
compensate for such currency price fluctuation. A well-known layer 1 blockchain which
supported a native stablecoin was the Terra blockchain. The Terra blockchain was known
as an algorithmic stablecoin, meaning both the stablecoin itself and the asset that backed
it (the Luna digital currency in this case), are issued by the layer 1 blockchain. Layer 1
blockchains such as Terra, which support algorithmic stablecoins, rely on a set of
algorithms that maintain its stablecoin price by dynamically increasing and decreasing
the supply of the backing digital currency to compensate for changes in demand. It
should be noted that the Terra blockchain halted service in 2022, and at the time of this
writing, none of the 10 layer 1 blockchains mentioned in figure 2.3 support a layer 1
issued stablecoin. An alternative to an algorithmic stablecoin, is stablecoins that are
backed by government-issued currencies. Such government asset-backed stablecoins
are centralized however and are discussed in greater detail later in this chapter.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


21

SMART CONTRACT SUPPORT


Originally with Bitcoin, a transaction represented a cryptocurrency payment between two
Bitcoin wallets. Later projects expanded this capability so that a transaction could be
either an interaction between two wallets, or between a wallet and a smart contract. The
term smart contract refers to application code that is saved to a blockchain. For layer 1
blockchains that support contracts, the contents or programming logic of the contract is
also stored within the blockchains' blocks. A single block typically holds the initial
contents or state of the contract, with further uses of the contract referencing that initial
block and its associated assets. Because this code is saved into blockchain blocks that
are tamper resistant, the rules that are written into the smart contract are considered
binding like contracts issued by a traditional lawyer. Keep this analogy in mind if writing
smart contracts for your Web3 application. Although it is always possible to deploy a new
contract, once your contract is saved to a blockchain, it cannot be modified.
Although smart contracts have become synonymous with Web3 application
development, it is important to know that not all layer 1 blockchains support smart
contracts. Some layer 1 blockchains such as Monero and Zcash were designed to
function strictly as a currency and have remained in this format. Table 2.1 shows the
previous list of 10 popular layer 1 blockchains along with whether or not they support
smart contracts.

Table 2.1. A comparison of smart contract support among 10 popular layer 1 blockchains along with their
smart contract programming language

Smart contract support Smart contract language


Avalanche Yes Solidity
Binance Smart Chain (BNC) Yes Solidity
Bitcoin Limited Script
Ethereum Yes Solidity, Vyper, and LLL
Mina Yes Typescript
Monero No -
Near Yes Rust
Polkadot Yes Rust
Solana Yes Rust
Zcash No -

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


22

If a portion of your application logic needs to be stored inside of a contract, it will be


important to validate whether a blockchain’s contract language has sufficient flexibility to
support your application. Blockchains whose contract support is most mature may
require learning a programming language specific to their contracts in addition to the
programming language used to interact with their client software development kits
(SDK). The Ethereum blockchain is one such example of this. Depending on the intended
use of your application, there may already be certain programming languages that are
highly popular, such as Python for infrastructure, Javascript for web, or Rust for high-
performance applications. Choosing a blockchain platform that offers a client SDK in a
language that you intend to use for other parts of your application can reduce
development overhead.
For layer 1 blockchains which do support smart contracts functionality, this capability
can be handled either natively, or through a virtual machine.

VIRTUAL MACHINE COMPATIBILITY


A blockchain virtual machine is an operating environment that provides an abstraction
between running smart contract code, and the blockchain that sits underneath. One of
the largest advantages of running smart contracts within a virtual machine is that it
allows contracts to be written in programming languages that are different from the
underlying blockchain, or for a single blockchain to support smart contracts written in
multiple languages. When smart contracts are run inside of a virtual machine, they may
be cross-compiled at runtime, or virtualized using a series of APIs-like translations. For
blockchains that do utilize a virtual machine for smart contracts, the low-level
implementation will be unique to the individual blockchain.
Through providing abstraction and translation, virtual machines are also able to
provide smart contract compatibility between certain layer 1 blockchains. The most
common example of this is the Ethereum Virtual Machine (EVM). Some other layer 1
blockchains that have implemented virtual machines that are EVM compatible are
Binance Smart Chain, Avalanche, and Cardano.
Outside of smart contracts running natively and through virtual machines, it is also
possible to build an entire secondary blockchain on top of the capabilities provided at
layer 1. Systems that take this route are referred to a layer 2 blockchains.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


23

PRIVACY AND ZERO KNOWLEDGE CRYPTOGRAPHY


The first generation of blockchain platforms placed a heavy emphasis on transparency
and public access to activity records. At the other end of the spectrum are blockchain
systems that are designed to be private. These platforms often use zero-knowledge
cryptography and zero-knowledge proofs (ZKPs) to allow verifying data while not making
it public by default. A common example used to explain how ZKPs work is the game
"Where’s Waldo". In the game Where’s Waldo, a player is asked to find the cartoon
character Waldo within a densely filled page. Applying a ZKP to this game would involve
the player proving they know where Waldo is located, without revealing his location on
the page. To provide this proof, the player can cut out and return only the character
Waldo from the page.
Some additional examples of how blockchain can apply ZKPs, include proving a
person is old enough to drive without revealing their age, proving a person has a
university diploma without revealing the issuing school, or with digital currencies, proving
that a payment was made, without revealing the party that made the payment. The Mina
and Monero blockchains are examples of a layer 1 blockchains that support ZKPs. At the
time of writing, the Bitcoin and Ethereum blockchains do not support ZKPs natively,
although there are blockchain projects which aim to add ZKP support to Ethereum by
functioning as what is termed a "layer 2".

2.2.2 Layer 2 blockchains: faster and cheaper


Layer 2 blockchains most commonly focus on improvements in speed; an areas where
many layer 1 blockchains have poor performance. The term layer 2 was first popularized
with the Ethereum blockchain. Although there are layer 2 blockchains built for other
ecosystems (such as the Bitcoin Lightening Network), at the time of writing, a majority of
layer 2 systems existing today, have been built for Ethereum compatibility.
By being an early leader and center of development for layer 2’s, Ethereum has also
shaped the vocabulary used to categorize layer 2’s. The most common of these
categories are state channels, side chains, plasma chains, and roll-ups. Although each
category differs in implementation, at a high level, they all pursue improvements in
speed by offloading transaction activity from the underlying layer 1 blockchain and
governed by a parent smart contract that is deployed to that layer 1 blockchain. The
following sections provide additional information on how each achieves this.

STATE CHANNELS
State channels allow a fixed group of participants to send transactions to each other off-
chain. A state channel is opened by deploying a smart contract to a layer 1 blockchain,
which accepts an initial deposit from participants (figure 2.4). This smart contract
contains all logic that will govern interactions within the state channel. Note that
deployment of a root smart contract containing governing logic is the method used by all
layer 2 systems to handle settlement to layer 1 blockchains underneath.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


24

After the initial deposit, the state channel is considered open and participants can
complete transactions with each other outside of the layer 1 blockchain. All transaction
that happens outside of the layer 1 includes a sequence number as well as any balance
changes and cryptographic signatures from participants to certify authenticity. When the
parties have completed their activities and no longer need to transact with each other,
closing transactions are sent to the payment channel smart contract to settle the state
and final balances of the participants on the layer 1 blockchain. Within the state channel
smart contract, the logic can allow it to be closed by a single participant, all participants
or some combination in between. Figure xx shows a graphic representation of state
channel transaction flows.

Figure 2.4. A state channel can be opened between two or more participants, however, all participants
must be pre-defined within the opening layer 1 smart contract.

Although payment transactions are the most common use for state channels, they can
also be used with smart contract state changes. The Bitcoin lightning network is an
example of a layer 2 that utilizes state channels.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


25

SIDE CHAINS
A side chain, as the name implies, is an entire parallel blockchain. Side chains operate
almost entirely as standalone blockchains, but periodically record their state to an
underlying layer 1 chain to inherit the layer 1 blockchain’s security. You can think of this
as taking a backup and saving it onto the more secure storage provided by the layer 1
blockchain (figure 2.5). Because side chains are fully operational blockchains on their
own, they also come with a variety of consensus models, performance attributes, and
features; including smart contract support.
With a side chain, the periodic snapshot that is sent to the underlying layer 1
blockchain is determined entirely by its own consensus and security configuration and
only contains a representation of the layer 2 state at a point in time. A proof or
guarantee is not required; the configuration and security of the layer 2 is being trusted
to work properly.
A popular example of an Ethereum side chain is the Polygon blockchain. The Polygon
blockchain uses PoS consensus, similar to Ethereum, but can achieve much higher
throughput, estimated at 65,000 transactions per second compared to 30 transactions
per second at the Ethereum layer 1. Because Polygon is EVM compatible, smart
contracts written in the Solidity language for Ethereum are generally compatible with the
Polygon blockchain.

Figure 2.5. A layer 2 side chain can significantly improve throughput by submitting condensed summary,
or "checkpoint" blocks to the layer 1 blockchain underneath

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


26

PLASMA CHAINS
Plasma chains were designed as a more purpose-built side chain for the Ethereum layer
1 blockchain. A plasma chain works like a side chain, but with improved security and a
focus on non-fungible tokens (NFT).
A fungible item is any item that is not unique or where there exist copies of the item.
The US dollar is an example of an item that is fungible. A non-fungible item is considered
unique and not substitutable. In the context of blockchain, non-fungible items are
represented digitally on-chain as non-fungible tokens. A digital token on a blockchain can
be made non-fungible through the inclusion of unique metadata such as a serial number.

NOTE Beyond including unique identification, NFTs are digital tokens like any other token issued
on a blockchain. The programming required to include NFTs in a Web3 application is not
substantively different from other blockchain digital tokens, and for this reason, NFTs are not
covered in depth in this book. For more information about the history and development of NFTs,
visit: https://ethereum.​org/en/developers/​docs/standards/​t okens/erc-​721/.

When using a plasma chain, the snapshot that is placed onto the layer 1 blockchain
includes proof of the state, by providing a cryptographically signed record of all prior
transactions, up to the moment of the snapshot. Although a proof of state is provided,
the layer 1 blockchain does not perform verification on the proofs that are provided;
they are assumed to be valid. In addition to providing cryptographic proof of
transactions, plasma chains often include a settlement or "challenge" period of a week or
more, to allow transactions to be disputed, if the layer 2 blockchain is compromised or
there is a dispute regarding the states which were submitted.

ROLL-UPS
Layer 2 roll-up blockchains inherit the capabilities of both state channels and plasma
channels and add to these an ability to verify the cryptographic proofs that are submitted
as summaries to the layer 1 blockchain underneath. Layer 2 roll-ups achieve this by
programming into the governing layer 1 contract, additional logic that at a minimum can
validate that the transactions being referenced in the summary block as publicly
available. This guarantees that any parties that have transaction disputes have the data
to verify the correct state.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


27

Layer 2 roll-up blockchains come in two categories, based on whether the source
transactions are public or private. The first variety, optimistic rollups, supports the
minimum requirement of guaranteeing source transaction data is available publicly. In
the optimistic category are Ethereum layer 2 blockchains such as Arbitrum and
Optimism. These layer 2 blockchains are termed "optimistic" because they typically
operate consensus models with reduced security which is optimistic about the validity of
transactions and does not generate a cryptographic proof for each transaction. Because
of this, layer 2 optimistic rollups typically enforce a settlement period like Plasma chains.
The second layer 2 rollups is zero-knowledge rollups or zk-rollups. zk-rollups
guarantee that source transaction data is publicly available for verification. Unlike
optimistic rollups, however, zk-rollups provide the source transaction data in the form of
the zk-proofs which were described earlier in the chapter. Using this format has two
benefits, the first is that by generating a proof for each transaction, zk-rollups do not
require a settlement window for disputes; transactions are proven as they happen. The
second benefit is that by using a zk-proof specifically, the contents of the transaction are
allowed to remain private. Two examples of layer 2 zk-rollups are StarkWare and
zkSync. Both of these projects rely on Ethereum as their layer 1 blockchain.
Because the difference among layer 2 blockchains is minor, table 2.2 is provided as a
single summary of how the difference layer 2 blockchain implementations compare.

Table 2.2. A comparison of the most common layer 2 blockchain configurations

Optimistic
State channel Side chain Plasma Chain zkRollup
Rollup
Deployed with
a smart Yes Yes Yes Yes Yes
contract
Permissioned
Yes Yes No No No
participation
Permissionless
No No Yes Yes Yes
participation
Snapshots
include No No Yes Yes No
transactions
Proofs are
- - Yes No Yes
verifiable
Has
settlement Yes Yes Yes Yes ^ No
period

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


28

ADDITIONAL BLOCKCHAIN CONSIDERATIONS


On a spec sheet, reviewing raw throughput or other scaling measures may oversimplify
real-world performance. While throughput and scaling capabilities are core to pairing the
capabilities of a blockchain to the expected demands of a Web3 application - it is
important to remember that blockchains are decentralized systems that are changing in
time. In addition to stated performance numbers, analyzing performance stability over
time can provide great insight into a blockchain’s platform performance. Some questions
that can be asked to gauge stability are:
1. Has this platform completed any hard forks? If yes, how often do these
occur and why?
2. Has this platform experienced past failures or system halts?
3. Has the platform experienced network congestion? If yes, under what
circumstances? How was it resolved?
4. Is this platform a common target for attacks?

Answering these questions can give a more complete picture of the long-term health and
holistic performance that can be expected when choosing a blockchain to work with.

2.3 Not all blockchain is Web3


Although blockchain technologies have become synonymous with Web3, some systems
and services use blockchain but are in fact, not Web3. These are usually systems and
services that are controlled and operated by a single company. The following two
sections outline systems that use blockchain but do not meet this book’s definition of
Web3, because they either are not peer-to-peer or do not support verifiable self-
sovereign identity.

2.3.1 Centralized Finance


Centralized Finance (CeFi) refers to the entire category of systems and services that
interface cryptocurrencies with the traditional banking system. The most common
examples of CeFi systems are centralized cryptocurrency exchanges such as Coinbase
and Binance. Cryptocurrency exchanges such as these facilitate buying and selling
cryptocurrencies on a user’s behalf and may include additional services that are centrally
managed, such as price dashboards that are specific to the exchange. User accounts
created at CeFi cryptocurrency exchanges are owned by the exchange. A CeFi exchange
can also deactivate or deny services to a user account at any time. The identities are not
self-sovereign.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


29

CeFi providers may also offer services beyond buying and selling cryptocurrencies. A
CeFi service that has gained popularity in recent years is the issuance of stablecoins. A
stablecoin is a cryptocurrency that is designed to have a stable price, and most
commonly achieves this by being backed by other assets such as the US dollar.
Stablecoins may even be backed by other cryptocurrencies or non-currency assets such
as real estate. Stablecoins are designed to address the issue of price volatility that is
common among cryptocurrencies; when an exchange or coordinated entity manages
assets to stabilize the price, the stablecoins cannot be considered Web3.

NOTE There do exist stablecoins which are Web3 and re-balance backing assets automatically
using smart contracts. These cryptocurrencies are referred to as "algorithmic stablecoins".

2.3.2 Blockchain infrastructure as a service and APIs


To make interacting with blockchain and Web3 applications easier, some companies now
offer blockchain infrastructure as a service (IaaS) as well as APIs. Examples of this
include Amazon Web Services (AWS), which offers "Amazon Managed Blockchain", and
Google Cloud’s "Blockchain Node Engine". Although these systems can be used to deploy
and interact with layer 1 blockchains such as Ethereum, the service that is being
purchased is the management platform and hardware resources are provisioned and
controlled by the cloud provider. Such infrastructure as a service and API solutions
remain firmly part of the Web2 domain.

2.4 What about free Web3?


A glaring omission in many discussions relating to Web3 is how the current bulk of Web2
that is offered for free can make the transition. It is difficult to find a definitive
calculation for the portion of internet sites or internet traffic that is accessible with a
direct fee, but an unscientific way to gain context would be to review your personal
browser history. What portion of the sites visited do not require payment? Including a
blockchain in your Web3 project can also mandate unnecessary payment mechanics that
are unnatural for the intended application use.
This gap presents an opportunity for the development of Web3. It is now well
understood that one way Web2 services can be offered for free is by asking users to pay
with personal data or through advertisements. For many, these formats are undesirable,
but they serve an important point to demonstrate that there are ways that web services
can be paid for, that are not direct monetary payment. Are there other ways users can
pay for services indirectly in Web3? One way is through participating in the sharing of
data or otherwise operating as a portion of the infrastructure that makes a peer-to-peer
system work. A successful Web3 system strikes the correct balance between what a user
feeds into a system and the utility they receive.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


30

To further explore the idea of a Web3 that is free, the next section of this book takes
a “blockchain optional” approach to Web3 application architecture and will teach you the
Web3 application architecture of a free streaming music service named Code Radio.

2.5 Summary
Blockchain allows peer-to-peer applications to support new services
such as payments and identity, due to its tamper resistance.
Blockchains are often categorized as layer 1 or layer 2. Layer 1
blockchains perform slower and have higher security, while layer 2
blockchains are typically faster and are paired with a layer 1 blockchain
to improve security.
TPS speed can vary greatly among blockchain projects, ranging from 6
TPS on the low end to highs above 8,000+ TPS.
Layer 1 blockchains can run smart contract code either natively, or
within a virtual machine.
Some layer 1 blockchain projects are built to be EVM compatible and
can support Ethereum smart contract code.
Zero knowledge cryptography and ZKPs enable layer 1 blockchains
such as Mina and Zcash to support private transactions.
State channels are a layer 2 blockchain technology that enabled a fixed
group of participants to complete transactions off-chain and record a
final balance to a smart contract when all activities are complete.
Side chains, plasma chains, and roll-ups are examples of layer 2
blockchain systems.
Layer 2 roll-ups blockchains are available in two varieties: optimistic
and zero knowledge.
Not all systems and services that use blockchain are Web3. Some
examples of these include centralized finance applications as well as
blockchains-as-a-service.
Including a blockchain within a Web3 application is optional. Web3
applications that are free and do not include blockchain are an area
that is currently underdeveloped.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


31

3
Designing applications that
run client-side

This chapter covers


Application design of an example Web3 music service named "Code Radio".
Using Javascript as a replacement for centralized cloud compute
Programming foundations of a Web3 application using Javascript
Handling music playback using Javascript and the HTML Audio API

This chapter details the application architecture of an example application; a Web3 music
streaming service named Code Radio . Code Radio is a Web3 system designed to take
advantage of the benefits of running peer-to-peer, but does not include a blockchain or
digital tokens. Code Radio is an example of a Web3 application that is designed to be
accessed without payment, similar to a majority of internet services today.
Additionally, this chapter introduces the Javascript programming that underpins the
Code Radio Web3 application, including accessing the HTML Audio API for music
playback.
Whether it is a single page or a complex multi-layered application, if a system needs
to display dynamic content in a browser, it is very likely that it relies on Javascript to
handle this processing. Because of the ubiquity Javascript has earned as a default
programming language for Web, I’ve chosen it for the programming of Code Radio and
as a showcase for the construction of applications for Web3.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


32

With almost three decades having passed since its initial release in 1995, Javascript
has a very large and still expanding ecosystem of libraries, frameworks, and tooling that
allows building applications as complex as a fully featured operating system. To lower
the barrier for reuse however, this chapter makes use of standard, or vanilla Javascript.
The code examples presented in this chapter are intended to present a starting point that
can and should be reused in later projects with the frameworks and libraries of your
choice.

3.1 Code Radio: Defining a feature set


You begin the Code Radio application architecture by defining its feature set. A feature
set is a definition or high-level description of the functionality you want an application to
have. Describing what you want your application to do in its final form has a number of
benefits, including allowing you to estimate the overall complexity of your idea, as well
as modify, iterate, and test your ideas with users ahead of making a large investment in
the development of the application code.
Each feature that is included in your feature set should have a specific intended use
that solves a user problem. For example, if a user needs to keep track of time while
traveling, one solution to this would be to create a world clock application. For this
application to solve the user’s problem it must include the ability to display the time for a
set of user-defined locations. Including the ability to access GPS location may also make
sense to automatically display a local time, in addition to the times at locations that have
been manually set.
If you are unsure where to start or whether certain functionalities are critical enough
to be included in your feature set, you can also constrain your scope through defining
the problem you are solving using the Five W’s of who, what, when, where, and why . By
following this format, you ensure that your Web3 application at the design phase, has a
specific target user (who), specific workflow (what), specific usage context(when and
where), and a specific problem being solved (why) to improve your chances of organic
adoption among the user community.
For the World Clock application the Five W’s are:

Who: Business travelers


What: Track time in more than one location
When: While traveling
Where: While in transit and at the office
Why: To schedule meetings and calls with participants in other locations

NOTE For more information about defining application feature sets, see User Story Mapping:
Discover the Whole Story, Build the Right Product (https://www.​oreilly.com/​library/view/​user-
story-​m apping/9781491904893/)

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


33

Returning to our Code Radio example; the problem we are solving is the difficulty of
finding music to play in the background which can help users focus while programming.
Framing this in the form of Five W’s becomes:
Who: Programmers
What: Background music
When: While programming
Where: At a desktop or laptop computer
Why: To reduce distractions and improve focus

After using the Five W’s to narrow the applications' focus, the following items become a
minimally required feature set to solve the users' problem:
The ability to play and pause music
The ability to choose between two or more radio stations

For Code Radio, the music being played are recordings that are in the public domain.
Public domain music refers to songs not protected by copyright which do not require
payment for performance or distribution. For this reason a blockchain to track payment
or ownership is not required. Code Radio does however, still benefit from a Web3
architecture of running peer-to-peer. With a minimum feature set defined, the following
section maps this to a user interface design.

3.2 Code Radio: User interface


One of my favorite quotes on user interface design comes from Martin LeBlanc, CEO of
Iconfinder, who says, "A user interface is like a joke. If you have to explain it, it’s not
that good."
Designing a user interface (UI), whether for cloud or Web3, is a practice of combining
visual design, interaction design, and information architecture :
Visual design: The look of the system. Visual design is concerned with
the styling of the aesthetic elements of a system. This can include
typography, sizing, color, spacing, and further visual elements that
provide hints or otherwise guide the users' expectation for how they
can interact with a system and how the system will respond.
Interaction design: The feel of the system. Interaction design is the
planning and design of how a user will interact with or manipulate a
system. This can include decisions such as whether the system is
engaged with using touch, a keyboard and mouse, or as an interface
that runs entirely in the background without direct user interaction.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


34

Information architecture: The detail of the system. Information


architecture covers the organization and structure of the data
presented by a system. This can include structures for organizing,
navigating, labeling, and searching the contents of a system.
Information architecture connects the context (the visual and
interaction design) to the content (the underlying data being served).

NOTE Interface design is a broad field and is presented here only at a high-level in the context of
the Code Radio application. For more information about application design, see Design for
Developers (https://www.​m anning.com/​books/design-​for-developers).

Figure 3.1. The code radio interface is single-purpose, with no additional menus or navigation. The user is
asked to learn a single interaction of clicking or touching a channel. Interactions for starting, stopping,
and changing channels are handled using this single interaction.

With consideration given to its visual design , interaction design, and information
architecture, figure 3.1 shows the chosen Code Radio UI. At this point in the architecture,
you now have a defined feature set and have mapped these features to an interface
design. The next step in Code Radio’s application architecture is to identify the
programming language(s) that will be used in order to plan the packaging, distribution,
and infrastructure required for the application to operate. Remember that this all
happens before writing the your first line of code.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


35

3.3 Code Radio: Application architecture


By combining the context provided in the prior sections, a holistic view of the Web3
application architecture for the Code Radio reference application is possible. The
architecture makes an assumption that Code Radio will be written as a web application
using HTML, CSS, and Javascript, and made open source to allow community
contributions. For easy reference, table 3.1 shows a summary of how Code Radio is
configured using the application architecture inputs covered in this chapter.

Table 3.1. With knowledge of the architecture differences between a centrally-managed application and
an application designed for Web3, the design chosen for Code Radio then becomes the input that defines
its application architecture.

Code Radio Architecture Inputs Description


Ability to play and pause music; ability to
Feature Set
choose between two or more stations
User Interface Static site containing three station buttons
Service Discovery Libp2p with bootstrap seed list
Open source code: HTML, CSS, and
Distribution
Javascript

Finally, figure 3.2 shows a second summary, this time, for the connectivity of a new user
starting the application for the first time, as well as the existing connectivity among
participants who have previously discovered each other using the application’s
embedded seed list. In final form, all centrally-managed cloud infrastructure is replaced
within the Code Radio application design.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


36

Figure 3.2. By building Code Radio for wbe browser compatibility, a users machine resources helps
increase the capacity of the application network as long as they have their browser open.

3.4 The code behind Code Radio


Because the function of HTML and CSS does not change significantly when used in Web3,
they are not covered in detail in this book. To enable the creation of the Code Radio
demo application however, the completed HTML and CSS code is available on GitHub and
can be downloaded as a starting point for the Javascript details that are covered in this
chapter. Web developers may be familiar with the content of this chapter, but I am
providing it as a review of JavaScript client-side functionality and also as a starting point
to build upon in the next chapters, in which you will progressively re-implement Code
Radio to operate as a peer-to-peer application.
Before going any further, visit appendix A and follow the instructions to download the
Code Radio application code and install the Node.js tools which you’ll need for this book.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


37

3.4.1 Launching the project code


With the project code downloaded and Node.js installed, you can now run Code Radio.
Navigate to the following directory from the command line by running the following
command:

cd ./web3-application-architecture/chapter_3/chapter_3_begin/code_radio

From within this directory, start the Node.js web server using the command:

http-server

With this command, Node.js starts a web server on your local machine and return output
that shows the IP address and port that it has assigned. Listing 3.1 shows the output of
running this command on Mac OS.

Listing 3.1. Launching http-server

code_radio % http-server
Starting up http-server, serving ./

http-server version: 14.1.1

http-server settings:
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

Available on:
http://127.0.0.1:8080 #1
http://192.168.125.169:8080 #1
Hit CTRL-C to stop the server

#1 Your application web address.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


38

NOTE If you have executed the previous instructions on a dedicated server or already have
another web server operating on your machine, it is possible that the default http-server port of
8080 is not available. To resolve this, an alternative port can be specified by including "-p [PORT
NUMBER]" after the web server command. For example "http-server -p 8081". For a complete list
of settings that can be used with http-server, visit the package documentation page on NPM at:
https://www.​npmjs.com/​package/http-​server.

Now that your web server is online, copy and then paste one of the web addresses from
the output into your web browser. The Code Radio application should load as shown in
figure 3.3.

Figure 3.3. As a starting point, the Code Radio interface has been provided. At this stage Code Radio is
just a static web page with no functionality. Additional Javascript is required to turn this web page into a
functioning application.

After you have loaded Code Radio in your web browser, back at your command line, you
see additional log output showing resources being accessed on the page, similar to those
shown in listing 3.2. This output can be used for debugging your application by
confirming if the correct resources are being loaded by http-server as well as checking
for any errors reported by http-server in cases where your application code does not
function as expected.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


39

Listing 3.2. Http-server log output

"GET /" "Mozilla/5.0 (Macintosh; ..."


"GET /static/radio.css" "Mozilla/5.0 (Macintosh; ..."
[2022-10-02T19:12:15.002Z] "GET /radio.css" "Mozilla/5.0 (Macintosh; ..." #1
[2022-10-02T19:12:45.369Z] "GET /" "Mozilla/5.0 (Macintosh; ..."

#1 Http-server loading page styling located in file "/radio.css".

NOTE In future projects, if http-server loads a directory in the web browser instead of your Web3
application, this generally means that the index.html file that is used as a starting point by http-
server is not found. Ensure that your applications' starting screen is named index.html and that
you launch http-server from the directory containing the index.html file to avoid error.

If you have not done so already, take a moment to click the radio buttons and interact
with the Code Radio. The application is not functional in this case, because additional
Javascript is required to power the logic and interactions of the application. You begin
adding this Javascript functionality in the next section.

3.5 Adding client-side compute to Code Radio


Applications written using Javascript code fall into one of three categories, determined by
where the application code is run. These are client-side , server-side , or a hybrid of the
prior two.
With client-side Javascript, all of the applications' Javascript code is visible to and run
by the user. An application that runs entirely using client-side Javascript does not require
support or external resources from a web server. An example of this would be a
calculator application that has all the rules required to perform calculations held within
the Javascript code stored on the users' machine. If the calculator in this case is loaded
from a web page, the calculator would retain 100% of its functionality after the web page
is downloaded and the server that delivered the initial webpage goes away.
Server-side Javascript is the opposite of client-side. This is Javascript code that is
hidden from the user and runs on a web server. This type of code is most commonly
referred to as "backend" code and is often used to build application programming
interfaces or APIs which third party applications make requests to. Examples of this are
the Google Maps and Amazon Web Services Javascript APIs. Server-side Javascript
typically does not include a graphical user interface because its is other application code.
Hybrid Javascript applications make use of both client-side and server-side Javascript.
This type of Javascript application is used in cases where both the client-side and server-
side resources are required and managed by a single party. An example of this would be
a photo uploading service such as Google Photos. In this example, the provider (Google)
manages both the Javascript delivered to the user (the user interface) as well as the
code that stores the photos in database storage that is hidden from the user.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


40

A primary benefit of running an application that operates peer-to-peer is that the


central server that is otherwise in-between, gets removed. This structure is core to Web3
architecture and a prerequisite of removing centralized control. In order for your Web3
application to operate without a web server, any application logic that would traditionally
run from a centralized web server, must be moved to the client in the form of client-side
Javascript code. When doing this, all compute resources required of the application are
services by the end users' machine. This architecture component is referred to as client-
side compute .

Figure 3.4. Web3 applications remove all centralized servers from the application flow.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


41

In this section you add the client-side Javascript logic that allows the Code Radio
application to handle music playback. Adding the additional logic to the Code Radio
application starts by creating a Javascript code file and updating the existing HTML page
to load it.
To add Javascript to the Code Radio application, create and save a new file in the
project root directory named "radio.js". The project root folder should now contain a
media folder, as well as files radio.css, index.html, and radio.js.
Before adding any Javascript to the new file, return to the file index.html and add the
following line as the last item of the HTML body:

<script src="./radio.js"></script>

Adding this line instructs the HTML page to also load the contents of the Javascript file
when the page loads. In a web application, Javascript is render blocking, meaning the
browser waits until it has completely loaded the defined Javascript file before loading any
parts of the HTML page that come after. For this reason the Javascript file is placed at the
end of the HTML body, allowing the Code Radio UI to fully load before the loading of the
script. The updated index.html file should now match listing 3.3.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


42

Listing 3.3. Code Radio index.html

<!doctype html>
<html>
<head>
<title>Code Radio</title>
<meta name="description" content="Free streaming radio for programmers">
<meta name="keywords" content="Web3 tutorial application">
<link rel="stylesheet" href="./radio.css">
</head>

<body>
<div class="main">
<div class="radio_text">
<h1>Choose Your <span class="radio_header">Vibe</span></h1>
<div class="radio_parent">
<div class="radio_box"><button class="bn30" id="btn-
classic">Classic</button></div>
<div class="radio_box"><button class="bn30" id="btn-
ambient">Ambient</button></div>
<div class="radio_box"><button class="bn30" id="btn-
lofi">LoFi</button></div>
</div>
<br>
<p>Code Radio is a decentralized demo application for the book Web3
Application Architecture <br><br>
(Steven Platt, Manning Publishing 2023)</p>
</div>
</div>
<script src="./radio.js"></script> #1
</body>
</html>

#1 Javascript file import.

For this chapter, this is the only time updating the HTML code is necessary. The
remainder of this chapter happens entirely within your Javascript file radio.js. The sky is
the limit for the level of customization and compute operations that you can add in as
Javascript. For the Code Radio application, you are adding the following functionality:

Interactive UI channel and selection logic


Media playback using the HTML audio API
Accessing user storage to save media for offline playback

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


43

3.5.1 Adding an interactive UI and logic for channel selection


One of the most common uses of Javascript is to manipulate the interface of a webpage
to provide visual feedback for an action the user has taken. For example, displaying a
form when an edit icon is pressed, or animating a progress bar after a file upload. In this
section you are using Javascript to change the Code Radio interface when a user makes
channel selections; specifically, you are changing the font color when a user selects a
channel by clicking it. In Javascript, this logic is handled by event listeners.
An event listener monitors an assigned item on a webpage and waits for a specific
event to happen before it executes its logic. For Code Radio, the item being monitored is
a channel button, the event that you are waiting for is a click, and the logic is executed
by calling a function which updates the buttons' font.
Within the HTML document, the first radio button has an ID of "btn-classic" assigned.
To manipulate the button using Javascript, you must select the HTML button by using its
ID, and assign that selection to a variable which can be interacted with further using
Javascript. Once you have assigned the button object to a variable, an event listener can
then be connected to the variable, which links it to the button element within the HTML
UI.
Returning to the radio.js file, to add create an event listening for the Classic radio
button, the first step is to add two lines of code. The first line being the HTML selection
using ID btn-classic and the second to create an event listener for that item which takes
two inputs: the event trigger, and the function that is called by the trigger (listing 3.4).

Listing 3.4. Adding an event listener to the Classic radio button

const classic_audio = document.getElementById('btn-classic'); #1


classic_audio.addEventListener("click", play_classic); #2

#1 Selecting the button object


#2 Registering an event listener on the button object

For the remaining two radio buttons; within the HTML file, the "Ambient" radio button
carries an ID of "btn-ambient", while the "Lofi" button carriers an ID of "btn-lofi".
Because each of the radio buttons are operated separately, you also need to create
similar event listeners for the other two buttons, using their unique HTML IDs. After
adding these event listeners, all three of the buttons within Code Radio will be
programmed to call an associated Javascript function when clicked. When completed
your event listener Javascript code should match that in listing 3.5.
Within your radio.js file, update the code to assign the HTML ID btn-ambient to a new
variable named "ambient_audio". Followed by the corresponding event listener which
after a click event, calls the Javascript function "play_ambient".
Finally, for the Lofi button of Code Radio, update the code to assign the HTML ID btn-
lofi to a new variable named "lofi_audio". Followed by the corresponding event listener
which after a click event, calls the Javascript function "play_lofi".

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


44

Listing 3.5. Completed event listener code for all three radio buttons

const classic_audio = document.getElementById('btn-classic');


classic_audio.addEventListener("click", play_classic);

const ambient_audio = document.getElementById('btn-ambient'); #1


ambient_audio.addEventListener("click", play_ambient); #2

const lofi_audio = document.getElementById('btn-lofi'); #3


lofi_audio.addEventListener("click", play_lofi); #4

#1 Selecting the btn-ambient button object


#2 Registering an event listener on the btn-ambient button object, which invokes the play_ambient
function
#3 Selecting the btn-lofi button object
#4 Registering an event listener on the btn-lofi button object, which invokes the play-lofi function

Once you have registered the event listeners, they will be fully functional only after you
have implemented the corresponding functions invoked by each of them, namely
play_classic(), play_ambient() and play_lofi(). These functions modify the button font
after the button has been clicked.
By changing the font on the radio button when it is clicked, you are giving a visual cue
to the user for which radio channel is active. With the new Javascript functions, the font
is changed by using the classList.add method to add the CSS class "playing" to the
buttons' font as shown in listing 3.6.

Listing 3.6. Logic to change the font of the "Classic" radio button when clicked

function play_classic() {
document.getElementById("btn-classic").classList.add("playing");
}

The Javascript logic in listing 3.6 adds a minimum of functionality to the Classic radio
button. You could repeat this same function for the other buttons, but the Code Radio
application would not have logic to ensure only a single button was active at a time, or
that the user is able to click the button a second time stop a station. Before adding
additional functions for the other two buttons, the original play_classic Javascript function
should be updated once more to add the previously mentioned logic. As a start, add a
new global variable named "classic_playing" to your radio.js file, which stores a boolean
value. For now, this variable can be assigned a default value of false . This
classic_playing variable is global in this case because it will be accessed by other
functions.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


45

From within the play_classic function, you will now add logic that will check the value
of the classic_playing variable to decide what to do. This is done using if/else logic to
handle if classic_playing if true , if it is false , and a final case to pass or skip the function
logic if there is an error. Based on these cases, you then add or remove the CSS class
shown in listing 3.6, as well as add or remove the the CSS class from the other radio
buttons. To visualize the logic, consider these three scenarios:
Scenario 1: If nothing is playing and a radio station button is pressed,
you update the status of the station to confirm it is now playing as well
as change the CSS font applied to the radio station button text.
Scenario 2: If a station is already playing, and the same radio station
button is pressed, the playback is paused. The playback status of not
playing, and the additional CSS font is removed from the station button.
Scenario 3: If a station is already playing, and a different radio station
button is pressed, the playback of the first station is paused. The first
station status is changed to not playing, and the additional CSS font is
removed from the first station button. For the new station button that
was pressed, you update the status of the station to confirm is now
playing as well as change the CSS font applied to the radio station
button text.

Take a moment to apply this logic for the Classic radio station and its corresponding
play_classic function. After updating the logic and including the new classic_playing
global variable, your Javascript function should match the code in listing 3.7.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


46

Listing 3.7. Updated play_classic logic that includes the button state

var classic_playing = false;

function play_classic() {
if (!classic_playing){
classic_playing = true; #1

document.getElementById("btn-classic").classList.add("playing");
document.getElementById("btn-ambient").classList.remove("playing"); #2
document.getElementById("btn-lofi").classList.remove("playing");

} else if (classic_playing){
classic_playing = false;
document.getElementById("btn-classic").classList.remove("playing");

} else {pass;}
}

#1 Updating the playback status.


#2 Updating button CSS.

Additional code can now be added for the play_ambient and play_lofi functions. When
these are added, the three functions and additional variables match the code included in
listing 3.8. The play_classic function you just completed has been omitted for brevity.

Listing 3.8. Completed UI logic for the Classic, Ambient, and Lofi Code Radio buttons

var classic_playing = false;


var ambient_playing = false;
var lofi_playing = false; #1

...

function play_ambient() {
if (!ambient_playing){
ambient_playing = true;

document.getElementById("btn-classic").classList.remove("playing");
document.getElementById("btn-ambient").classList.add("playing");
document.getElementById("btn-lofi").classList.remove("playing");

} else if (ambient_playing){
ambient_playing = false;
document.getElementById("btn-ambient").classList.remove("playing");

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


47

} else {pass;}
}

function play_lofi() {
if (!lofi_playing){
lofi_playing = true;

document.getElementById("btn-classic").classList.remove("playing");
document.getElementById("btn-ambient").classList.remove("playing");
document.getElementById("btn-lofi").classList.add("playing");

} else if (lofi_playing){
lofi_playing = false;
document.getElementById("btn-lofi").classList.remove("playing");

} else {pass;}
}

#1 The application now tracks the state of playback for all three buttons

The Javascript powering Code Radio now has a fully interactive UI with logic to change
each button font when pressed. The additional Javascript logic that was added enables
support for play , pause , and change station features. At this time, you can press Ctrl+C
to stop your instance of http-server if you have not already done so, and reissue the
"http-server" server command to restart it.
Visiting the url in your browser shows the updated version of Code Radio using client-
side compute to handle interactivity in the UI. Notice the behavior of the station buttons
as they are clicked and the station is changed. Your version of Code Radio should match
figure 3.5 with a new yellow font for the selected station.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


48

Figure 3.5. Code Radio now has an interactive UI that changes font colors when the buttons are clicked.
Notice how changing stations by clicking different buttons removes the font styling from the previously
active option.

NOTE By default, most browsers cache downloaded Javascript code. If your local instance of Code
Radio does not behave correctly, try clearing your browser’s cache before restarting http-server
and visiting the application URL.

Code Radio looks slick at this point in its programming, but its most important capability
is still missing; the actual music streams. The following section details the code to handle
audio playback using the HTML Audio API.

3.5.2 Handling music playback with the HTML Audio API


HTML is natively able to handle the playback of both audio and video content. When
audio is placed directly within HTML, it is handled using an <audio></audio> tag. The
HTML Audio API provides an abstraction of this HTML audio element to be consumed and
manipulated by external code such as Javascript. For the Code Radio application, the
HTML Audio API is used to begin music playback when a station button is clicked.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


49

In an identical manner to how an event listener was used to update the Code Radio
interface after a click event, an event listener is required here to start a music playback
stream after a station button is clicked. Because both items are being triggered by the
same click event, the event listeners that were programmed in the prior sections can be
reused here. For convenience, the original event listeners have been reproduced in
listing 3.9.

Listing 3.9. The original event listeners are reused here and are not modified

const classic_audio = document.getElementById('btn-classic');


classic_audio.addEventListener("click", play_classic); #1

const ambient_audio = document.getElementById('btn-ambient');


ambient_audio.addEventListener("click", play_ambient);

const lofi_audio = document.getElementById('btn-lofi');


lofi_audio.addEventListener("click", play_lofi);

#1 Changing the action triggered by the event listener requires updating the Javascript function that it
calls when triggered.

Recall that once an event listener is triggered, the remainder of the application logic is
held within the Javascript functions that are called. These functions are play_classic,
play_ambient, and play_lofi. It is here that the additional programming to start and stop
music playback should reside.
Before expanding controls within the playback functions, the music source itself must
be defined within the Code Radio Javascript. Using the HTML Audio API, this is done by
declaring new global variables outside of the playback functions. The HTML Audio API
can be used to either with a file on the local disk, or a remote file. In this case, you will
use a remote file as shown in listing 3.10.

Listing 3.10. Defining audio sources for Code Radio

var classic_stream = new Audio("https://storage.googleapis.com/telecomsteve-


website/audio/trap_and_ink.wav"); #1
var ambient_stream = new Audio("https://storage.googleapis.com/telecomsteve-
website/audio/trap_and_ink.wav");
var lofi_stream = new Audio("https://storage.googleapis.com/telecomsteve-
website/audio/trap_and_ink.wav");

#1 Declaring a playback source.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


50

NOTE Music used for the Code Radio application has been shared for reuse with the permission of
the musics creator Antwan Griffin. More information about the artist can be found on his LinkedIn
profile page at https://www.​linkedin.​com/in/antwan-​griffin-60aa85196/.

After defining audio sources for Code Radio, the sources can now be used within the
Javascript playback functions using the .play() method of the HTML Audio API. If the
playback is already active and the station button is clicked again, audio playback is
paused using the .pause() method of the HTML Audio API.
Again starting with the Classic station, audio playback can be added to its
corresponding playback function by editing the play_classic function in your radio.js
Javascript file to account for the two scenarios previously mentioned. When complete
your code for the play_classic function should match listing 3.11.

Listing 3.11. Defining audio sources for Code Radio

function play_classic() {
if (!classic_playing){
classic_stream.play(); #1
classic_playing = true;

document.getElementById("btn-classic").classList.add("playing");
document.getElementById("btn-ambient").classList.remove("playing");
document.getElementById("btn-lofi").classList.remove("playing");

} else if (classic_playing){
classic_stream.pause(); #2
classic_playing = false;
document.getElementById("btn-classic").classList.remove("playing");

} else {pass;}
}

#1 Starting audio playback.


#2 Pausing audio playback.

If at this point you pause to test the Code Radio application, you may notice another
limitation in the Javascript logic. Based on the code in listing 3.11, if a station is already
playing music and you click the button of a second station, there is currently no logic to
stop playback on the other station. This desired enhancement as well as looping audio
playback can be achieved by again updating the playback functions within your radio.js
file to use some additional methods provided by the HTML Audio API.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


51

Starting first with the HTML Audio "loop" method. This method tells the HTML Audio
API to restart playback if it reaches the end of the file and is applied to the audio source.
For example, for the play_classic function, this would be applied to the classic_stream
audio source. Take a moment to apply the loop method to each of the audio sources
defined in your Javascript playback functions. This should be done outside of the existing
if/else logic so that it applies in all cases.
The second HTML Audio method required to complete the described logic is the
"currentTime " method. This method tells the HTML Audio API to reset the playback
position of the audio source to the beginning and is also applied to the audio source. To
always start station playback from the beginning, apply the currentTime method to the
audio sources defined in your Javascript playback functions. Again, this function needs to
be applied outside of the existing if/else logic so that it applies in all cases.
Adding the previous logic brings you to a state where the Code Radio station buttons
can play and pause, loop the audio source, always start playback from the beginning, as
well as change the button text to confirm the playback state. The last step of preventing
two stations from playing at the same time requires logic that matches the logic has
already been implemented for adding and removing CSS styling to the radio buttons.
Indeed, the existing if/else logic can be reused here and updated to add the play() and
pause() methods to control the music sources in addition to the existing controls for
adding and removing the CSS button styling. For convenience the original logic scenarios
are reproduced here to help visualize where play() and pause() logic would be placed
within your playback functions:
Scenario 1: If nothing is playing and a radio station button is pressed,
you update the status of the station to confirm it is now playing as well
as change the CSS font applied to the radio station button text.
Scenario 2: If a station is already playing, and the same radio station
button is pressed, the playback is paused. The playback status of not
playing, and the additional CSS font is removed from the station button.
Scenario 3: If a station is already playing, and a different radio station
button is pressed, the playback of the first station is paused. The first
station status is changed to not playing, and the additional CSS font is
removed from the first station button. For the new station button that
was pressed, you update the status of the station to confirm is now
playing as well as change the CSS font applied to the radio station
button text.

After updating the logic for the Classic station, your play_classic function should match
listing 3.12.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


52

Listing 3.12. Using additional .loop, .pause(), and .currentTime controls from the HTML Audio API

function play_classic() {
classic_stream.loop = true; #1

ambient_stream.pause(); #2
ambient_stream.currentTime = 0; #3

lofi_stream.pause(); #2
lofi_stream.currentTime = 0; #3

if (!classic_playing){
classic_stream.play();
classic_playing = true;

document.getElementById("btn-classic").classList.add("playing");
document.getElementById("btn-ambient").classList.remove("playing");
document.getElementById("btn-lofi").classList.remove("playing");

} else if (classic_playing){
classic_stream.pause();
classic_playing = false;
document.getElementById("btn-classic").classList.remove("playing");

} else {pass;}
}

#1 Using HTML Audio to loop playback.


#2 Limiting playback to a single station.
#3 Setting playback to always start from the beginning.

NOTE The HTML Audio API does not support a .stop() method. Although the .pause() method can
be used in isolation; here it is being used in tandem with .currentTime=0 as a substitute for the
missing .stop() functionality.

This full playback control logic should now be added for the play_ambient and play_lofi
playback functions as well. After adding the additional playback controls, your Javascript
code should match the completed chapter code located within this books github
repository within directory chapter_3/chapter_3_end/code_radio/.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


53

Code Radio is now a functioning application with all logic and computation happening
client-side. A foundation of Web3 application architecture is the remove of all centralized
resources and by moving all application logic into the client application, you have fully
removed the need for a remote web server relying on centrally managed compute from
Code Radio.
At this time, any user who downloads a copy of your current Code Radio application is
able to run the application without restriction, but you may have noticed that while the
user interface code if fully local, the music that is streamed is not, it remains centralized
and coming from a single source. It is lucky in this case then, that Javascript also have
provisions for handling local storage. In the next chapter you will use Javascript to save
the Code Radio music files locally, making the Code Radio application fully self-contained
before programming it connect and share information peer-to-peer in a later chapter. If
you have completed the programming tasks presented in this chapter but have an
unexpected result, you can compare your code with the completed chapter code within
the chapter_3/chapter_3_end/code_radio/ to assist in debugging.

3.6 Summary
Using the Five W’s of who, what, when, where, and why can help to
narrow the scope and define the feature set of your Web3 application.
Existing processes of defining a feature set and developing a user
interface can be reused when building applications for Web3.
Javascript can be used to complete all application computation and logic
directly on the client machine.
The Node.js Javascript runtime can be used in tandem with the http-
server package to test Javascript programming during development.
Javascript event listeners can be used to respond to user interactions
within an application.
Both local files and remote audio sources can be played using the HTML
Audio API.
The HTML Audio API lacks a "stop" method. This behavior can instead
be created by using .pause() and .currentTime methods.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


54

4
Using Javascript for client-
side storage

This chapter covers


Using Javascript as a replacement for centralized cloud storage
Handling local file storage using the Javascript Web Storage and IndexedDB client-
side storage APIs

In this chapter, you’ll continue working with the Javascript programming of the Code
Radio Web3 application from Chapter 3 to add client-side storage to allow it to function
fully offline. Modern web browsers support a number of mechanisms for applications to
store and retrieve data from a user’s local machine. This chapter focuses on two of these
mechanisms: using the Javascript Web Storage and IndexedDB APIs.
Storing data on the user’s local machine can be useful in a number of scenarios,
including but not limited to the following:
Storing user preferences and site customizations, such as UI themes.
Storing and maintaining user activities such as a shopping cart status
between application sessions.
Storing general application data such as image, music, and video
assets locally, so that they load faster in future uses.
Storing general application data such as image, music, and video
assets locally, so that they can be used without an internet connection.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


55

In this chapter, the Javascript Web Storage API is used to store the Code Radio user
preference of a light or dark UI theme, whereas the Javascript IndexedDB API is used to
store the Code Radio music files on the users' local machine. Using Javascript for client-
side storage in this format allows the Code Radio application to function offline and
without centralized cloud storage after its initial boot.

4.1 Storing Web3 application preferences using the Web Storage API
The Web Storage API is a browser API that allows storing simple data, such as strings,
and numbers in the form of key:value pairs on a client’s machine. The limited format of
data that can be stored using Web Storage API makes it ideal for storing simple state
data such as application variable values.
The Web Storage API supports two types of objects, sessionStorage and localStorage .
The first format is used to store data that is useful only during the active session and can
be discarded at the end of a session. A session can be 5 minutes, 5 hours, or longer. An
example of this would be media that is buffered or preloaded during file playback. If you
have ever received a prompt from a streaming service or your bank, asking "Are you
still there"; the purpose of this mechanism is to trigger the clearing of the user session
and related sessionStorage.
The latter item, localStorage, is used to store items that need to be persisted to the
users' local machine after they have closed the application and session. An example of
this would be application configurations such as time zone, which is useful and valid data
for more than a single session. In general, the types of data saved into localStorage
using the Web Storage API could just as easily be saved to a centralized application
database stored in the cloud. Two important cases that arise which could make using
localStorage preferred are for privacy-focused applications and applications that are
designed to run offline or without central control.

NOTE At the time of writing, the Chrome, Firefox, and Safari web browsers limit the total size of
data that can be stored using localStorage to 5MB. This is a shared limit that is applied to all
websites and applications sharing a domain name or IP address.

When designing privacy-focused applications, one way to provide guarantees for privacy
is to process and handle user data on-device. Handling user data on-device also carries
a secondary benefit of limiting the potential for unintended mixing or exposure of user
data in ways that could violate regulations such as the General Data Protection
Regulation (GDPR) in the European Union or the Personal Information Protection Law
(PIPL) in China. Code Radio sits within the second category of applications suitable for
localStorage because it is an application designed to operate without a central control. In
Code Radios' case, you are using it specifically to store the user’s choice of application
theme—specifically, whether a new dark theme is enabled or not.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


56

4.2 Adding dark mode to Code Radio


Tracking the state of the Code Radio application theme can be done by declaring a single
"theme" variable in Javascript and assigning it a value of "light" or "dark." This value can
then be checked by other parts of the application so that the theme colors are applied
when the page loads.
A limitation that arises in this scenario however is the fact that the "theme" variable
would be initialized with the default value each time the Javascript application loads, so
whatever is stored in the variable while the application is running is not saved.
Traditionally, this lack of data persistence is resolved by storing the value of the "theme"
variable in an external database, one that is not on the user machine and instead is
operating from a centralized cloud provider. For your Web3 application to remain
operating outside of a centralized cloud provider, any application storage, in addition to
the application logic, must be moved to the client. To allow the Code Radio application to
retain this data locally, the localStorage function of the Web Storage API is used.
If you have not already done so, the practice code used for this chapter can be
downloaded from GitHub using the instructions within Appendix A. As with the previous
chapter, because this chapter’s focus is Javascript, the completed HTML and CSS code
used for this chapter has been provided and is located at the repository location web3-
application- architecture/chapter_4/chapter_4_begin/code_radio/.
To conceptualize how the new dark theme is intended to be used within the Code
Radio application, start by navigating the previously mentioned repository location and
launch a Node.js http-server session as shown in listing 4.1. The resulting output returns
the IP address and port that is assigned.

Listing 4.1. Launching http-server

code_radio % http-server
Starting up http-server, serving ./

http-server version: 14.1.1

http-server settings: #1
CORS: disabled
Cache: 3600 seconds
Connection Timeout: 120 seconds
Directory Listings: visible
AutoIndex: visible
Serve GZIP Files: false
Serve Brotli Files: false
Default File Extension: none

#1 The running configuration of http-server

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


57

The resulting output returns the IP address and port that is assigned.

Available on:
http://127.0.0.1:8080 #1
http://192.168.125.169:8080 #1
Hit CTRL-C to stop the server

#1 Your application web address

In a browser, navigate to 127.0.0.1:8080 to view the revised interface of the Code Radio
application. As a starting point, the HTML and CSS have been updated so that now the
application user interface displays a theme toggle in the upper left corner (figure 4.1).
The new theme toggle that is added will be used to allow a user to switch between light
and dark themes within the Code Radio application.

Figure 4.1. As a starting point, the Code Radio user interface has been provided. At this stage Code Radio
is a static web page without functionality. Additional Javascript is required to turn this web page into a
functioning application.

At this point, the theme toggle is non-functional; as you still need to implement the
underlying Javascript logic. You will add this additional programming in the following
section.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


58

4.2.1 Using the localStorage API for simple application storage


The localStorage API (as well as the previously mentioned sessionStorage) supports five
methods:

setItem()
getItem()
removeItem()
clear()
key()

You can add data to the localStorage through the setItem() method, which takes two
arguments in the following format:

localStorage.setItem("<key>", "<value>");

You can query existing data through the getItem() method, which takes a single
argument in the following format:

localStorage.getItem("<key>");

You can remove a single item from localStorage through the removeItem() method,
which takes a single argument in the following format:

localStorage.removeItem("<key>");

You can remove all items from localStorage through the clear() method, which does not
require any arguments:

localStorage.clear();

Last, in cases where iterating or looping through all entries in localStorage is desired,
you can use the key() method, which takes a single argument in the following format:

localStorage.key(<index>);

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


59

CAUTION

localStorage does not require or support authentication for access. For this reason,
sensitive data should never be stored using localStorage. For additional detail on
the localStorage specification and implementation, visit the Mozilla Developer
Network documentation page at developer.​mozilla.org/​e n-US/docs/​
Learn/JavaScript/​Client-side_​web_APIs/​Client-side_​storage.

To implement localStorage within Code Radio, start by creating a new Javascript file
named darkmode.js within the directory web3-application-
architecture/chapter_4/chapter_4_begin/code_radio/js/.
Similar to the process used in the previous chapter to change the Code Radio button
font, toggling between the dark and light theme within Code Radio can be done by
toggling the application of a new "dark-theme" CSS class, which darkens the appearance
of elements within the Code Radio UI. Within the new darkmode.js file, a new function
named "toggle_theme" must be added to execute this. Within the function, a local
variable can be used to set the theme to "light" or "dark" before that value is saved to
localStorage as shown in listing 4.2. Take a moment to add this completed code to your
darkmode.js file.

Listing 4.2. The toggle_theme function which darkens the Code Radio UI

function toggle_theme() {
document.body.classList.toggle("dark-theme");

let theme = "light"; #1


if (document.body.classList.contains("dark-theme")) {
theme = "dark";
}
localStorage.setItem("theme", theme); #2
}

#1 Declaring the chosen theme.


#2 Saving the chosen theme to localStorage.

Although the function to change the Code Radio theme between light and dark is in
place, it is not yet connected to or called from the physical toggle button that is within
the Code Radio UI. To connect these two, a small amount of additional Javascript code is
needed.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


60

To connect the new button UI to your Javascript file, a new variable named btn is
required, which selects the UI theme toggle within the HTML and stores it within the
Javascript. To achieve this, the Javascript built-in document.querySelector() method is
used. This method is required for this specific case because you are targeting a CSS
class that applies to multiple items which are changed when the dark theme is applied.
This is in place of document.getElementById() which you used in the previous chapter to
select a single item (the text on a single channel button) identified by an HTML id.
The new btn variable can be declared by adding the following line of code to your
darkmode.js file:

const btn = document.querySelector(".btn-toggle");

Now that a variable is defined that selects the UI theme toggle within the Code Radio UI,
as you did in the previous chapter, add a Javascript event listener to your darkmode.js
file, which triggers the toggle_theme() function during a click event:

btn.addEventListener("click", toggle_theme);

With the theme toggle and corresponding Javascript function now linked and saving to
localStorage, Code Radio can read the theme that is set in localStorage when it is
launched. To complete this functionality, add an additional variable named
"currentTheme" to darkmode.js, which reads the set theme from localStorage as well as
an if statement that adds the "dark-theme" CSS class if the theme has been set to
"dark." When completed, the additional code block should match listing 4.3.

Listing 4.3. Reading the theme setting from localStorage

const currentTheme = localStorage.getItem("theme");

if (currentTheme == "dark") {
document.body.classList.add("dark-theme");
}

For security, data saved to localStorage is scoped to the domain. This means that a
localStorage key:value pair stored for web3-application-1 cannot be read by web3-
application-2. The isolation also allows keys such as "username" to be used within many
Web3 applications without conflict. Further, because localStorage is a browser API, this
means that the actual file that is written to the user’s hard disk is written and managed
by the browser running your Web3 application. Both Chrome and Firefox browsers save
this data into an SQLite database as part of their application directories.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


61

NOTE If the user removes or reinstalls the browser on their local machine, any data saved using
localStorage is removed as part of the browser.

After implementing this section’s code within the darkmode.js file, the Code Radio
application can be run again by issuing the "http-server" command and returning it to the
web browser. Your application should now be able to toggle between light and dark
themes while saving the chosen setting to your local machine. To test your
implementation of localStorage, try setting the Code Radio application to the dark theme.
After this, close the application and reopen it. The Code Radio application should now
directly load with the dark theme.
If for any reason, your local version of Code Radio does not load or toggle between
themes properly, you can also view the values being stored within localStorage from
within the developer tools of your browser GUI. Using the Chrome browser as an
example, navigating to the "Application" section will display a "Storage" section that
includes localStorage. Expanding the localStorage category will display all configured
key:value pairs. These key:value pairs will also update as you toggle the theme within
the Code Radio UI as shown in figure 4.2.

Figure 4.2. Using the browser built-in developer tools allows viewing and debugging the storage used
within your Web3 application.

Beyond the theme setting that can be saved as a simple key:value data structure, the
Code Radio application also handles playback of music files, which cannot be stored
client-side using localStorage. For more complex data, such as music and video files, the
IndexedDB API can be used.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


62

4.3 Using the IndexedDB API for complex application storage


Javascript using localStorage is not able to scale beyond key:value pairs by default. To
store more complex data including music, videos, and compressed archives, IndexedDB
can be used. When working with an IndexedDB database, a schema must first be
defined, and then it can be interacted with by first opening a connection to the database
and then issuing any sequence of transactions that perform read or write operations.
Similar to localStorage, IndexedDB data can only be accessed from its origin, which is
often the hosting web domain, or in this case, the IP address of your local machine; it
cannot be accessed across sites.
Unlike localStorage, which is limited to 5MB, IndexedDB is allowed to use significantly
more disk space and does not have a fixed storage limit. Calculating the total storage
space IndexedDB is allowed to use varies by browser. Firefox, for example, limits global
disk usage to 50% of disk for all IndexedDB instances, with an additional limit set to 20%
of the global limit for individual origins or domains.

NOTE When designing your Web3 application, be sure to confirm your application data model
matches the specific IndexedDB disk limits set by the browsers you intend to support.

IndexedDB is popular for storing complex data client-side, in part because it is a native
part of the HTML5 standard, allowing it to receive broad browser support. More bespoke
alternatives to IndexedDB are also available to add further capabilities, including RxDB
which uses a NoSQL model on top of IndexedDB, and PouchDB , which implements
CouchDB client-side, on top of IndexedDB. IndexedDB can be very powerful, and as a
result, also complicated. This section provides a general introduction to IndexedDB,
before IndexedDB support is added to Code Radio later in the chapter.
Although IndexedDB doesn’t support the SQL language, its usage workflow may be
familiar for developers who have interacted with database libraries and tools such as
SQLite or SQLAlchemy. At a high level, the usage pattern recommended with IndexedDB
can be understood as five steps:

1. Create or open an existing database.


2. Create or open an object store inside the database. The SQL equivalent
of this is the creation of a table.
3. Initiate a transaction, such as a read or write operation against an
object store.
4. Wait for a response from IndexedDB.
5. Perform an action based on this response.

The following sections expand and provide examples for each of the workflow steps.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


63

4.3.1 Creating a new IndexedDB database


Creating and accessing a database using IndexedDB are done in the same way—using a
Javascript request object that is mapped to an IndexedDB open() method. Just as with a
traditional database, IndexedDB lives as a database outside of the application; for this
reason, connecting to IndexedDB is done using network requests, handled by the
Javascript request object in this case.

NOTE The request object is part of the Javascript Fetch API. If you are unfamiliar with the Fetch
API, additional information can be found at the Mozilla Developer Network website: developer.​
mozilla.org/​en-US/docs/​Web/API/Fetch_​A PI.

When using the IndexedDB open() method, if a database does not exist, IndexedDB will
create it. Two arguments are required for the open() method, the first is the database
name and the second is the version number, as shown in the following example:

const request = window.indexedDB.open("MyDatabaseName", 2);

NOTE IndexedDB does not support floats such as "2.7" for version numbers, a whole number is
required. If a float value is used for the version number, IndexedDB will round down to the closest
integer value; "2" in this case.

4.3.2 IndexedDB error handling


Because you don’t always control the behavior of resources external to your application,
a best practice is to implement error handling to compensate for cases where the
external resource malfunctions or is for any reason, not available. Listing 4.4 shows how
to use the built-in onerror and onsuccess callback functions of the Javascript request
object.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


64

Listing 4.4. An error handling example using IndexedDB

let db;

const request = indexedDB.open("MyDatabaseName");


request.onerror = (event) => {
console.error("Your request was unsuccessful");
};

request.onsuccess = (event) => {


db = event.target.result; #1
};

#1 Storing the result of the event trigger

When using IndexedDB in an application, the user is prompted to confirm whether they
would like to allow the application to use local storage. A user denying the use of storage
is the most common error generated by IndexedDB. Notice that in the previous example,
there is a new variable declared named db. In the case of a successful database
connection, the success event saves the result of that connection request in the new
variable db, so that it can be accessed and referenced elsewhere in the Javascript code.

4.3.3 Creating an object store inside an IndexedDB database


IndexedDB uses "object stores" in place of tables to store data. After an IndexedDB
database is created, adding an object store is required before any data can be saved.
IndexedDB relies on an onupgradeneeded event trigger for the initial creation of object
stores within the database. The onupgradeneeded event trigger is also used when
modifying an existing database, such as when changing the schema is required.
Listing 4.5 shows an example use of the onupgradeneeded event trigger to create a
new object store named "users", which uses "email" as the primary key.

Listing 4.5. Creating a new IndexedDB object store.

request.onupgradeneeded = (event) => {


const db = event.target.result;
const objectStore = db.createObjectStore("users", { keyPath: "email" }); #1
};

#1 Creating an IndexedDB object store

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


65

NOTE Similar to standalone databases, IndexedDB also supports creating indexes of object stores
so that other parts of records can be searched. For example, in the previous example, when
creating a new user, your application may also collect "first name" and "company" values. In this
case, an index can be created for company, which would then allow searching for users based on
either email, company, or both. Additional information for creating and managing indexes with
IndexedDB can be found by visiting the Mozilla Developer Network IndexedDB reference
documentation at: developer.​m ozilla.org/​en-US/docs/​Web/API/IndexedDB_​A PI/Using_​
IndexedDB#​creating_​and_structuring_​t he_store

Figure 4.3 shows a visualization of the relationship between IndexedDB, IndexedDB


databases, and IndexedDB objects stores.

Figure 4.3. A representation of the relationship between IndexedDB, databases, and object stores.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


66

Now that your IndexedDB object store has been created, you can perform CRUD
operations to both interact and build atop the IndexedDB storage.

4.3.4 Performing CRUD operations with IndexedDB


Applications that interact with a database are often referred to as CRUD applications
because they contain functionality that allows them to create, read, update, and delete
records inside of a database. With an IndexedDB database and object store defined,
these CRUD operations are now possible.
Interactions with an IndexedDB database are done using transactions. These
transactions have three levels of permissions: readonly , readwrite , and versionchange .
The later versionchange permission is a more advanced operation used for actions such
as schema changes and is not covered in this chapter. Outside of schema changes, most
IndexedDB operations are handled using readonly or readwrite permissions. When
performing a transaction against an IndexedDB object store, two arguments are used:
the object store name, and permissions level. If no permission level is defined, the
readonly permission is used by default. An object store name can also be an array; this
is because IndexedDB transactions can be applied to multiple object stores at one time.
This can be used in more complicated storage configurations, such as sharding. In that
application case, searching across multiple object stores to find data may be desired.
The following example shows a transaction using the previously created "users" object
store.

const transaction = db.transaction(["users"], "readonly");

NOTE Improve the performance of your application by using the readonly permission whenever
possible. Multiple readonly operations can be performed in parallel on an IndexedDB object store,
while readwrite operations place a lock on the object store for transaction safety and are
performed serially.

The following sections step through IndexedDB CRUD operations one at a time.

CREATE: ADDING DATA TO AN EXISTING INDEXEDDB OBJECT STORE


To demonstrate adding data to IndexedDB, listing 4.6 provides starting data.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


67

Listing 4.6. Example user records.

const customers = [
{ email: "jonathan@company.com", company: "The Billions Corporation"},
{ email: "ignacio@charity.org", company: "Local Charity A"},
{ email: "gizem@university.edu", company: "Bootcamp University"}
];

Using the same error handling format as before, the customer data can be iterated over
and added to the IndexedDB object store using the Javascript built-in forEach callback
function as shown in 4.7.

Listing 4.7. Iterating over the customer object to add new user records.

transaction.oncomplete = (event) => {


console.log("All new users added!");
};

transaction.onerror = (event) => {


console.log("Unable to add new users.");
};

const objectStore = transaction.objectStore("users"); #1


customers.forEach((user) => { #2
const request = objectStore.add(user); #3
request.onsuccess = (event) => {
console.log("1 user added!");
};
});

#1 Connecting to the "users" object store


#2 Iterating over all user records
#3 Adding each user to the "users" object store

Because IndexedDB handled transactions using Javascript events, the event result can
be stored for later use. In this case, the result that is returned is the defined keyPath or
primary key. Listing 4.8 shows an example of this using the customers.foreach()
function.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


68

Listing 4.8. Iterating over the customer object to add new user records.

const objectStore = transaction.objectStore("users");


customers.forEach((user) => {
const request = objectStore.add(user);
request.onsuccess = (event) => {
const result = event.target.result; #1
console.log("User " + result + " has been added!");
};
});

#1 Returns the defined keyPath for the record

After populating the database with data, the stored records can be queried, parsed, and
used throughput your Web3 application.

READ: QUERYING EXISTING DATA INSIDE OF AN INDEXEDDB OBJECT STORE


Retrieving data from an IndexedDB object store is done using get() after a transaction
connection has been established and the target object store has been defined, as shown
in listing 4.9.

Listing 4.9. Reading records from an object store within IndexedDB.

const transaction = db.transaction(["users"], "readonly");


const objectStore = transaction.objectStore("users"); #1
const request = objectStore.get("ignacio@charity.org"); #2

request.onerror = (event) => {


console.log("Record not found!");
};
request.onsuccess = (event) => {
console.log(`Welcome, employee of ${request.result.company}!`);
};

#1 Opening a connection to the object store


#2 Retrieving a record from the object store

Here the request result is not saved to an intermediate variable but is instead printed
directly to the console. If further processing will be done, such as updating stored
values, storing the request result into an intermediate variable is usually required and is
covered in the following section.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


69

UPDATE: MODIFYING EXISTING DATA INSIDE OF AN INDEXEDDB OBJECT STORE


You modify records within an IndexedDB object store by first reading a record, storing it
to an interim object variable, modifying the variable, and then finally using the put()
function to write the changed value to the IndexedDB object store. An example of this
workflow is shown in listing 4.10. In this listing, the record that was read in the prior
example is modified to change the associated "company" value.

Listing 4.10. Updating records within an IndexedDB object store.

const transaction = db.transaction(["users"], "readwrite"); #1


const objectStore = transaction.objectStore("users");
const request = objectStore.get("ignacio@charity.org");

request.onerror = (event) => {


console.log("Record not found!");
};

request.onsuccess = (event) => {


const data = event.target.result;
data.company = "Technology Inc";
const requestUpdate = objectStore.put(data);

requestUpdate.onerror = (event) => {


console.log("Could not update record!");
};
requestUpdate.onsuccess = (event) => {
console.log(`Welcome, employee of ${request.result.company}!`);
};
};

#1 readwrite permissions are required to update data.

Notice that the permissions defined for the transaction have also been updated from
readonly to readwrite, this is required for the put function to be able to write the record
back into the object store after it has been changed.
With the ability to edit records in place, a final resource in managing the lifecycle of
application data is the ability to fully delete records.

DELETE: REMOVING DATA FROM AN EXISTING INDEXEDDB OBJECT STORE


Removing records from an object store within IndexedDB is done using the delete()
function in a workflow similar to that used to update records. First, a transaction is
opened to IndexedDB, followed by defining the target object store before using the
delete() function on a specific record. An example of this flow is shown in listing 4.11.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


70

Listing 4.11. Deleting records within an IndexedDB object store.

const transaction = db.transaction(["users"], "readwrite"); #1


const objectStore = transaction.objectStore("users"); #2
const request = objectStore.delete("ignacio@charity.org"); #3

request.onerror = (event) => {


console.log("Record not found!");
};

request.onsuccess = (event) => {


console.log("The record has been deleted!");
};

#1 New connection with read and write permission


#2 Connecting to the "users" object store
#3 Performing a delete operation

If for any reason, IndexedDB CRUD operations are not behaving as expected. Using your
browser’s built-in developer tools can assist in debugging here as well. Using the Chrome
browser as an example; in the same manner that localStorage data was viewed earlier
in the chapter, you can view objects stored in IndexedDB by accessing developer tools
and navigating to the "Application" tab. Within this tab, IndexedDB storage can be
browsed as test CRUD operations.
With a firm grasp of creating an IndexedDB instance and completing CRUD
transactions against the database, one additional consideration to be aware of is
IndexedDBs lack of ACID compliance.

4.3.5 Considerations of ACID compliance


Traditional database systems such as SQL are designed to be ACID compliant, but this is
a standard that IndexedDB does not meet. ACID stands for atomicity, consistency,
isolation, and durability. IndexedDB supports atomicity, consistency, and isolation, but
lacks durability. If you are not familiar with ACID concepts and terminology, do not
worry, I explain what these mean and how they apply to IndexedDB in the following
sections.
For a database transaction to be atomic, it must guarantee that it will either fully
complete or fail all transactions - having no in-between state. It is quite common to
execute a database command that includes multiple sub-operations. An example of this
is a single transaction that performs two SELECT operations, an ORDER BY, and a MERGE
operation. If one of these sub-operations is not able to complete, a database supporting
atomicity can guarantee that the full transaction is reverted safely.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


71

IndexedDB supports atomic transactions but has a specific limitation that occurs when
a user unexpectedly closes the browser. Transactions that take a long time to complete
and are done in the background may be interrupted and reverted if the user
unexpectedly closes the browser. When designing your application to make use of client-
side storage using IndexedDB, it is important to be aware of this behavior. One way to
compensate for long-running operations is to notify the user directly. You may have
experienced this from existing applications, with messages similar to "updating… please
do not close or refresh the browser until updating is complete."
To deliver consistency, a database must enforce that all transactions committed to the
database are processed the same. IndexedDB guarantees consistency and uses a model
of strict serializability , which guarantees that operations are strictly handled in the order
that is defined in the transaction. It does not try to compensate, do error checking,
autocomplete, or otherwise compensate the order of operations within transactions.
Isolation in an ACID-compliant database guarantees that no two transactions can be
performed on the same resource. For example, if a store has five units of an item, and
six customers try to buy that item at the same time, isolation ensures that these orders
are completed one at a time, to correctly validate that the item is sold out after five
transactions. IndexedDB supports transaction isolation by limiting write access to object
stores to a single transaction at a time. When designing your Web3 applications, keep in
mind that read operations can happen in parallel and have no such limitation or locked
access. Only use readwrite transactions when necessary and not as a default.
IndexedDB does not support durability features such as replicas and fault tolerance.
Moreover, the stores of physical data within IndexedDB are part of the browser
application install. If the user of your Web3 application installs the application using
Firefox, IndexedDB places the data within a subdirectory of the Firefox application on the
user’s disk. If the user accesses your application from another browser, the state, and
data that was stored in IndexedDB within Firefox will not be accessible. This behavior is
the same as with browser cookies and caches but may be unexpected behavior for the
user, depending on the design of your application.

NOTE One way to ensure that your Web3 application is always used with the same browser
instance is to distribute it as a browser-specific extension. Another solution to compensate for a
lack of durability in client-side storage is to place portions of your application data within a
blockchain. Both of these items are topics covered in dedicated chapters later in this book.

Because Code Radio does not use many features of IndexedDB, completing an overview
of IndexedDBs' most commonly used features provides the baseline context to use
IndexedDB more widely and apply it to your own Web3 application ideas. The next
section narrows the scope of IndexedDB to only the features required to store the Code
Radio music files client-side and includes a few new considerations that are specific to
the Code Radio application architecture.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


72

4.4 Adding client-side storage for Code Radios music files


Now comes the time to see client-side storage do the heavy lifting. The Code Radio
demo application used in this book is designed to highlight some design considerations
that receive increased importance in Web3 application architecture. As part of
programming IndexedDB storage in the sections that follow, you approach the
boundaries of capability for client-side storage and experience how these are addressed
in Code Radio, specifically with file support and encoding.

4.4.1 Encoding music files for IndexedDB storage


When accessing a cloud database, or cloud blob storage, it is typically not visible to the
application developer what happens to files that are loaded, specifically how they are
processed. Connecting to and storing files to cloud storage is typically handled with a
user-friendly API that allows passing files as unmodified objects. After these files are
passed to the provider API, however, they often receive additional processing or
conversion to a format that can be placed into storage. When using IndexedDB, this
intermediate processing must be done by the application developer (figure 4.4).

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


73

Figure 4.4. A comparison of user-uploaded files (left) and files bundled within Web3 applications (right).

With the programming completed in the previous chapter, Code Radio was configured to
use audio sources that stream media from a web-addressable URL. In order to store
Code Radios music files in IndexedDB, they must first be converted into a blob (binary
large object) format that can be passed into IndexedDB. The standard method of doing
this is using the Javascript File API . Using the Javascript File API allows the user to
choose a file for input. The File API then supports reading this input as an array buffer,
binary string, or as text that is compatible with IndexedDB.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


74

An important distinction in this scenario is that the user is required to make a file
selection for this processing to happen. This workflow is restricted for improved security.
Imagine a scenario where loading a website or Web3 application allowed full access to
files on a user’s device! We definitely don’t want that. A key part of Code Radios
architecture is that it explicitly does not support user-generated content. To avoid illegal
file sharing, there is no mechanism for user file uploads, and by extension, no access to
the File API. Because of this design, Code Radios' music files must be encoded manually
and placed into IndexedDB.

NOTE IndexedDB relies on the structured clone algorithm built into Javascript to move files into
storage. The structured clone algorithm can support the storage of Javascript strings, arrays, sets,
array buffers, and more as well as Web API types such as blobs. For a full list of files types
supported by the structured clone algorithm and IndexedDB, visit developer.​m ozilla.org/​en-
US/docs/​Web/API/Web_​Workers_API/​Structured_​clone_algorithm.

Encoding the Code Radio music files involves the following steps:
Conversion of the .wav-formatted music files into base64 format.
Conversion of the base64 encoded media into a binary blob that is
compatible with IndexedDB.

Because manual file encoding is an outlier process, instructions and code for this step
have been placed in Appendix B. These instructions can be used both with Code Radio
and as a reference for future Web3 applications.
Before beginning the encoding steps, return to the programming exercise files for this
book. Starting at directory web3-application-
architecture/chapter_4/chapter_4_begin/code_radio/ add a new file to the js directory,
named "indexedDB.js". Within this file, all Javascript related to Code Radios' initial
IndexedDB configuration will be placed. As a simplification for now, Code Radio will use a
single .wav-formatted music file; this file is named "trap and ink" and is located in the
directory web3- application-architecture/chapter_4/chapter_4_begin/code_radio/media/.
Now, take a moment to complete the manual encoding steps provided in Appendix B.
At the end of this step, you will have a new audio source named "audioURL.src" which
points to a blob URL that can be stored in IndexedDB.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


75

4.4.2 Storing encoded files into IndexedDB


To place the audio files within IndexedDB, you will reuse the workflow presented earlier
in the chapter starting with declaring a JSON-formatted object named "musicFiles" which
holds all of the songs which have been converted to binary blobs. This is followed by the
workflow of creating an instance of IndexedDB and the child object store where the
music files will be placed, followed by a transaction that physically copies the music files
into IndexedDB. To achieve this, you can reuse the same code introduced in the previous
example. A completed version of the workflow is shown in listing 4.13. Take a moment to
add this to your indexedDB.js file.

Listing 4.12. Storing your encoded wav music files within IndexedDB.

const musicFiles = [{ #1
name: "trap and ink",
url: blobUrl,
sourceData: blob
}];
const dbName = "music_database";
const request = indexedDB.open(dbName, 1); #2

request.onerror = (event) => {


console.log("There was an error creating the database!");
};

request.onupgradeneeded = (event) => {


const db = event.target.result;
const objectStore = db
.createObjectStore("music_files", { keyPath: "name" }); #3

objectStore.transaction.oncomplete = (event) => {


const musicObjectStore = db
.transaction("music_files", "readwrite")
.objectStore("music_files"); #4
musicFiles.forEach((name) => {
musicObjectStore.add(name);
});
};
};

#1 JSON formatted declaration of music files.


#2 Creation of IndexedDB instance.
#3 Creation of child object store with "name" as the primary key.
#4 Transaction to copy the music files into IndexedDB.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


76

At this moment, you can save and close the indexedDB.js source file. The file now
contains all programming required to handle the conversion and storage of the original
wav-formatted music files used by Code Radio. For convenience, the Code Radio HTML
page has already been updated to include and load the Javascript file name
"indexedDB.js".
By loading this source file with the Code Radio UI, if the IndexedDB and object store
instance do not already exist, they will be created and the blob containing the Code
Radio music data will be saved to the user’s local disk. You are able to confirm that this
occurs by issuing the http-server command to serve the Code Radio instance and
returning it to your browser. Before navigating to the Code Radio URL, open the
developer tools of your browser and navigate to the "Application" menu. This menu
contains a "Storage" section, which lists all storage options available, including
IndexedDB.
With the developer tools menu open, navigating to the Code Radio URL shows the
IndexedDB instance created and populated in real-time as shown in figure 4.5. WebSQL
may also be shown as a storage option within your browser; note that this method of
storage has been deprecated in favor of storage using IndexedDB.

Figure 4.5. The application menu within the browser developer tools allows viewing the IndexedDB
instance, object stores, as well as the data that has been stored by the Code Radio application.

In the next section, the second half of Code Radio’s IndexedDB programming is added to
handle the reading and playback of the music files that were just stored.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


77

4.4.3 Retrieving music files from IndexedDB for playback


The remainder of the programming for this chapter happens within the already existing
radio.js file that was created in the previous chapter. This is because the last remaining
task to fully migrate Code Radio to client-side storage is to reconfigure the radio streams
that you defined previously; setting them to instead readout and play their station
streams from IndexedDB. Completing this programming also happens in two steps.
The first is defining the function that is used to read songs from IndexedDB, and the
second is the reimplementation of the Code Radio station buttons to use the new
function. Start the first step by declaring the database and object store that will be used
for music files. This will be placed at the top of your Javascript code and reuse the
database and object store names that were used for the database creation in the
previous section, as shown in listing 4.13.

Listing 4.13. Defining the database and object store to use for Code Radio music files.

var database = "music_database";


var object_store = "music_files";

Next, add a Javascript function named getData() that handles reading music files and
updating the audioURL source that was defined in the previous section. The function in
this case is not specific to a station but is generic, taking as input the name of the song
that is requested for playback. There is also no return value for this function; instead,
only the audioURL source is switched.
Remember that when the IndexedDB object store was created in the previous section,
"name" (the song name) was set as the keyPath and is therefore unique. The structure
of your Javascript function will follow the format presented earlier in the chapter for read
transactions using IndexedDB. A completed implementation of the function is shown in
listing 4.14.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


78

Listing 4.14. Reading music files from IndexedDB.

function getData(song) {
const transaction = db.transaction([object_store]);
const objectStore = transaction.objectStore(object_store);
const objectStoreRequest = objectStore.get(song);

objectStoreRequest.onsuccess = (event) => {


const myRecord = objectStoreRequest.result;
console.log(`${myRecord.url}`);
const audioURL = document.createElement("audio");
audioURL.src = myRecord.url;
audioURL.loop = true;
};
};

To reconfigure the streams that are started by clicking the Code Radio button within the
UI, the three functions that are connected to the click event listeners of the UI must be
updated so that they call the read function of the IndexedDB database, to read the music
files that are now stored locally, rather than in the cloud. Start this second step by
commenting out, or removing the previously defined stream sources. For convenience,
these original audio sources are reproduced in listing 4.15.

Listing 4.15. Commenting out the original Code Radio audio sources.

// var classic_stream = new Audio("./media/trap_and_ink.wav");


// var ambient_stream = new Audio("./media/trap_and_ink.wav");
// var lofi_stream = new Audio("./media/trap_and_ink.wav");

Next, within the previously defined playback functions (play_classic(), play_ambient(),


and play_lofi()), comment out the logic that references the old audio sources which are
no longer available. An example of this logic is shown for the play_classic() function in
listing 4.16. Be sure to comment out this same logic for both the play_ambient(), and
play_lofi() functions as well.

Listing 4.16. Commenting out the logic connecting to the retired radio streams.

// classic_stream.loop = true;

// ambient_stream.pause();
// ambient_stream.currentTime = 0;

// lofi_stream.pause();
// lofi_stream.currentTime = 0;

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


79

At this time, all references to the previous audio sources have been removed and the
new sources within IndexedDB client-side storage can be added. Within each of the three
playback functions, add the logic to open a connection to IndexedDB as the first action.
If successful, this connection must call the previously defined getData() function, with the
name of the song that should be played as shown in listing 4.17.

Listing 4.17. Calling the getData function to read the "trap and ink" music file from IndexedDB.

const DBOpenRequest = window.indexedDB.open(database, 1);


DBOpenRequest.onsuccess = (event) => {
db = DBOpenRequest.result;
getData("trap and ink");
};

As a final refactoring, the play(), pause(), and currentTime functions and method can be
again added at their original locations, but this time, using the audioURL inside the
IndexedDB record as the playback source. Because you are no longer using separate
audio sources and instead are switching the audioURL pointing to a single source
(IndexedDB), it is no longer required to have the duplicate play(), pause(), and
currentTime functions and method for each Code Radio station. The updated and
condensed version for the play_classic() function is shown in listing 4.18.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


80

Listing 4.18. Updated logic to switch song playback while using IndexedDB.

function play_classic() {
const DBOpenRequest = window.indexedDB.open(database, 1);
DBOpenRequest.onsuccess = (event) => { #1
db = DBOpenRequest.result;
getData("trap and ink");
};

audioURL.pause(); #2
audioURL.currentTime = 0; #2

if (!classic_playing){
audioURL.play(); #2
classic_playing = true;

document.getElementById("btn-classic").classList.add("playing");
document.getElementById("btn-ambient").classList.remove("playing");
document.getElementById("btn-lofi").classList.remove("playing");

} else if (classic_playing){
classic_playing = false;
document.getElementById("btn-classic").classList.remove("playing");

} else {pass;}
}

#1 Calling the getData() function a successful database connection.


#2 Consolidated audioURL playback source

Using the capabilities of IndexedDB, Code Radio now stores music files on the client
machine. With this in place, both the application settings and music file are maintained
client-side which fully removes the need for a centralized cloud-hosted database.
Depending on the design of your Web3 application, bundling files in the way shown in this
chapter can become quite large and difficult to load. One way to compensate for this is
to bundle only a minimum of content with your Web3 application and rely on peer-to-
peer connections to other users of the application to gradually download and sync the
application’s full feature.
Adding resource discovery and spreading the load of your Web3 application in this
way opens a path to create more feature-rich and resource-intense Web3 applications.
In the case of Code Radio, this peer-to-peer connectivity unlocks the final feature of its
application architecture required to allow it to fit the definition of Web3. Adding this peer-
to-peer programming to Code Radio is covered in the next chapter.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


81

4.5 Summary
Persistent client-side storage in Javascript can be handled using
localStorage and IndexedDB.
localStorage is capable of handling simple key:value pairs and is
limited to 5MB of storage per application.
IndexedDB supports complex file storage with significantly higher
storage quotas that are determined by the users' local machine
capacity.
Intermediate file processing may be required to store complex files
using IndexedDB.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


82

5
Using Libp2p to connect
applications peer-to-peer

This chapter covers


Introduction to the Libp2p Javascript library
Using the Libp2p Javascript library to connect Web3 applications peer-to-peer
Replacing application DNS with peer IDs provided by Libp2p

In this chapter you will learn about the Libp2p Javascript library, and how to use it to
allow your Web3 applications to communicate peer-to-peer. After replacing centralized
storage and compute with client-side Javascript programming in previous chapters,
allowing instances of the Code Radio demo application to communicate peer-to-peer is
the final step in removing all centralized resources and delivering a product that can be
certified as Web3.

5.1 Libp2p features


Libp2p is an open-source project initially developed by Protocol Labs, which refers to
itself as a "modular network stack". As a network stack, Libp2p is designed to abstract
away much of the complexities which come with programming network services, but a
basic level of data networking is still required to make use of Libp2p in most application
designs. At the time of writing, Libp2p is offered in several programming language
implementations, including Go, Rust, Python, C++, and more. This chapter focuses on
the Libp2p Javascript implementation which allows it to be seamlessly integrated with
application running in a browser, such as Code Radio.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


83

As a networking stack, Libp2p supports a wide range of features that support different
network functions. A basic knowledge of data networking is most useful at the beginning,
when deciding if Libp2p can or should be used. A partial list of features supported by
Libp2p include the following:

5.1.1 Multiple transports


Libp2p supports connecting applications using several network protocols, including TCP,
WebRTC, and QUIC. Depending on the type and volume of data being transmitted, one or
more of these transports may be suitable in an application design. Choice of transport
can also be affected by additional factors such as connection stability and latency.

5.1.2 Protocol muxing


Muxing or multiplexing refers to taking many signals and combining them into one.
Libp2p allows using multiplexing to send multiple data streams over a single application
connection. In many protocols, an starting negotiation is required for both the sender
and receiver to agree on the settings they will use for their connection. When muxing is
used, this negotiation is only needed once, at the beginning. This lower overhead results
in reduced bandwidth usage and can also reduce response latency in certain application
messaging scenarios (figure 5.1).

5.1.3 Encrypted connections


Connections made using Libp2p are encrypted by default. This means that information
exchanged over Libp2p connection is protected from third parties and other applications
which may be running on a users machine.

5.1.4 Native roaming


Device addressing on the internet works in a manner similar to the mailing address for a
home in the physical world. If a message is sent in the mail, it must be addressed to a
specific location to arrive safely. When a web application is deployed to a cloud provider,
such a fixed address is assigned, allowing it to be reached over the internet. Returning to
the home example, imagine for a moment, that you want to send a letter in the mail, but
the address of the recipient changes randomly. How would you communicate? For a
Web3 application that is running entirely client-side, this scenario is what happens when
the user device connects on a new network; it is issued a new IP address. Libp2p
automatically handles these address changes without requiring additional programming
to allow Web3 applications to maintain connectivity even as user addresses change.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


84

Figure 5.1. Before data can be sent between application instances, a certain amount of overhead is used
to establish the connection and negotiation options such as encryption. By using a single stream
connection, Libp2p is able to avoid this overhead when sending multiple requests.

NOTE Network programming is an entire domain of expertise in itself and is not exhaustively
covered in this book. For a broader introduction to data-networking concepts, I recommend you
read "Learn Cisco Network Administration in a Month of Lunches" by Ben Piper, www.manning.​
com/books/​learn-cisco-​network-administration-​in-a-month-​of-lunches.

It should be noted that because Libp2p has multiple implementations, there may not be
feature parity among the different implementations. As an open-source project, Libp2p
also receives regular updates, so the previous feature list should be taken as a sampling
of Libp2p’s capabilities, rather than an exhaustive list.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


85

5.2 Libp2p protocol support


As web browsers have matured and become more secure, a portion of the improved
security has come from restricting access, rather than opening it up. This comes into
direct conflict with Web3 applications that run in a browser and are intended to
dynamically discover and be available for interactions with new and unknown peers.
Because Libp2p serves as the layer responsible for peer discovery and network
connection, understanding the connectivity options available and the browser limitations
that affect Libp2p is crucial for gaining a foundational understanding of the
communication types that are possible in Web3.
Libp2p splits its supported connectivity into two categories: standalone node and
browser node . A Libp2p standalone node runs directly on a host machine. An example of
this would be a desktop application that is installed by an operating system and includes
Libp2p. Several blockchain projects rely on Libp2p and fall into this category. A few
examples of these are Ethereum, Polkadot, and the Mina blockchains.
A Libp2p browser node sits entirely within a web browser. Due to access and
communication restrictions imposed by modern browsers, Libp2p browser nodes are
significantly less common, and at the time of writing, can be considered the leading edge
of Web3 development. Web3 applications that use the Javascript implementation of
Libp2p can be implemented either as a standalone node (when running using Node.js) or
a browser node. The Code Radio application you will update in this chapter is a hybrid, it
relies on Javascript to handle its application UI and song storage within the browser,
combined with Node.js which permits the full peer discovery and connection abilities of
the Javascript implementation of Libp2p. The following sections detail the difference of
both using Libp2p within a browser and as a standalone node.

5.3 Connectivity available to Libp2p browser instances


When configuring Libp2p to operate within a browser, your Web3 application inherits
browser restrictions for secure contexts. Secure context is a web standard that refers to
the minimum standard for authentication and confidentiality that should be enforced by
browsers. Most modern browsers, including Chrome, Firefox, and Safari adhere to the
secure contexts standard. The secure contexts standard requires the use of certificates
for identification, sending of messages over TLS-encrypted connections, and a number of
other security-focused restrictions. An important restriction that extends from the
standard is the elimination of connections from unknown origin in the form of cross-
origin blocking. At a high-level, this means that peer-to-peer network discovery
exclusively within the browser is largely not possible.

NOTE The Secure Contexts web standard is a proposal of the World Wide Web Consortium (W3C).
A complete version of the standard, maintained by the W3C, can be found online at: w3c.github.​
io/webappsec-​secure-contexts/.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


86

Although full peer-to-peer operations are not permitted exclusively in the browser, it is
possible to use a parent connection that was established through a secure context, and
reuse it to send later communications peer-to-peer. This is how applications such as
video conferencing systems are able to stream content between participants. The
following section provides further detail of the WebRTC protocol capabilities, which are
inherited by Libp2p.

5.3.1 WebRTC
WebRTC is best known as the protocol that enables video calling for applications such as
Zoom and Google Meet. Connections that are made using WebRTC, whether for video
calling or otherwise, rely on a signaling server during communication to resolve peer
addresses. When signaling servers are centrally managed by a single entity (such as
with Zoom and Google Meet), WebRTC is considered a client-server model, rather than
full peer-to-peer. In the video call example, two browsers that join a single video call,
exchange signaling messages with a WebRTC server, which informs the browsers where
they are to send video data. Once the browsers have each other’s address, the video
portion of the call is sent using a data channel that is peer-to-peer (figure 5.2). WebRTC
is a W3C internet standard and pre-dates the secure contexts standard. For this reason
web browsers permit WebRTC to establish communication using self-signed certificates of
identity to support encryption of communication between peers.

Figure 5.2. A WebRTC implementation which relies on centrally-managed signalling servers is considered
a client-server architecture, rather than peer-to-peer.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


87

Libp2p as a protocol, supports establishing connections using WebRTC; when doing so, it
is bound by the underlying client-server mechanics of the WebRTC protocol. This means
that when using Libp2p to communicate browser-to-browser over WebRTC, the initial
connection signaling must still occur with a WebRTC server. In Web3 application
architecture, the role of the WebRTC server can be filled by multiple independently-
managed Libp2p standalone nodes to allow the application to operate fully peer-to-peer
(figure 5.3).

Figure 5.3. Libp2p supports making connections using WebRTC. In such cases, the role of WebRTC server
can be filled by multiple independently-operated Libp2p standalone nodes to allow the application to
continue to operate fully peer-to-peer.

Beyond WebRTC, Libp2p also supports newer WebSocket (used in conjunction with TCP)
and WebTransport (used in connection with QUIC) connections. In both of these cases, a
certificate from a recognized certificate authority is required to validate the domain used
by the application. Because an authorized certificate authority would introduce a level of
centralization, and because most instances of peer-to-peer applications will not have a
unique domain name assigned, using WebSocket and WebTransport connectivity with
Libp2p is not recommended at this time.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


88

5.4 Connectivity available to Libp2p standalone instances


Libp2p connectivity that is available only when configuring Libp2p as a standalone
instance includes:

5.4.1 TCP protocol support


Transmission Control Protocol (TCP) is the most widely used network protocol on the
internet. TCP was the first protocol supported by Libp2p but is only available when using
Libp2p in a standalone or hybrid configuration. Libp2p relies on TCP for connecting to
network peers and negotiating connection settings such as encryption. Because of its
continued wide use, the TCP protocol is considered the most compatible option across
home, mobile, and office/enterprise networks. Enterprise networks in particular may
block protocols or have specific port restrictions for protocols that rely on UDP.

5.4.2 QUIC protocol support


Quick UDP Internet Connections (QUIC) is a protocol initially designed by Google and can
be considered a more modern substitute for TCP. QUIC includes some enhancements
over TCP, including native encryption. By including additional capabilities such as
encryption by default, QUIC reduced the signaling overhead and round trip messages
that otherwise would be required to establish these features in new connections using
TCP. The Libp2p project recommends QUIC as the default connection protocol, however
at the time of writing, Libp2p supports QUIC only as part of its Go language
implementation. It is not available when using Libp2p in Javascript.

5.4.3 Hole punching


Systems that are accessible from the public internet may be contacted directly using TCP
or QUIC-based connections. If a system is behind a firewall, however, additional work is
needed to negotiate a successful connection. When a system operates behind a firewall,
it is typically able to place outward connections but is restricted from receiving inbound
connections to prevent malicious access. A very large portion of devices connecting to
the internet fall into this category, including a majority of mobile phones and home
internet connections that have not been explicitly assigned a public IP address by the
connecting internet provider. Libp2p refers to the additional negotiation to connect to
these systems as hole punching, as a reference to the hole or path that is opened
through the firewall to allow external inbound connections.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


89

Libp2p handles hole punching by using relays. A Libp2p relay is an instance of Libp2p
that is publicly reachable over the internet. For Web3 applications that rely on Libp2p,
having at least one instance of the application reachable over the public internet allows
instances that are behind a firewall to relay their communication to other instances by
going through the public node. Typically, only the initial connection signaling uses the
relay connection, with all traffic after this being able to travel peer-to-peer. In this
format, hole punching behaves similar to WebRTC as shown in figure 5.4, with additional
capabilities such as network address translation (NAT) to enable locating endpoints that
have their IP address changed by a firewall.

Figure 5.4. Libp2p standalone nodes support hole punching, which behaves in a manner similar to the
WebRTC protocol with additional capabilities such as NAT to support peer-to-peer connectivity among
applications run behind a network firewall.

In the spirit of removing centralization, a Web3 application could be considered to be in a


healthy state when multiple distributed instances are reachable from the public internet
which are operated by independent participants. The more of these public connections
there are, the more resilient the Web3 application becomes.

NOTE For additional detail on hole punching and the role of NAT in peer-to-peer communications
read the paper "Peer-to-Peer Communication Across Network Address Translators" from the
Massachusets Institute of Technology: pdos.csail.​m it.edu/papers/​p2pnat.pdf

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


90

Before progressing to implement Libp2p within Code Radio, the following section
showcases an example Libp2p configuration of a network ping to help support the
connectivity information that was just introduced.

5.5 Libp2p ping: code example


Ping is a common network operation in which a small discovery message is sent to a
recipient who then responds with an equally short reply. Ping operations rely on the
Internet Control Message Protocol (ICMP) and are most commonly used to measure
network performance and assist in troubleshooting. When an internet-connected system
goes offline for any reason, a ping message can be sent to determine if the system has
malfunctioned at higher application layers, or if it is fully unreachable over the network,
such as during a power outage. This section will walk through implementing a similar
functionality using Libp2p in place of ICMP. By implementing the ping example, you will
both build and test basic connectivity between instances of Libp2p. The application code
used within this ping example will also be reused and extended to connect Code Radio
application instances using Libp2p.
This tutorial begins within the companion Github code repository for this book. If you
are revisiting this chapter in isolation, or for any reason do not have a copy of the Git
repository on your local machine, take a moment to clone the code repository from
github.com/​stevenplatt/​web3-application-​a rchitecture.​git.
Start by navigating from the command line to the code repository location /web3-
application-architecture/tree/main/chapter_5/chapter_5_begin/libp2p_ping/. Earlier in
this book, the Node Package Manager (NPM) was used to install the http-server package
to allow local testing of the Code Radio application. In this earlier use, the http-server
package was not part of the Code Radio application, and until now, there was no
dependency or requirement to use NPM. In this tutorial, a number of NPM modules must
be installed as prerequisites to using Libp2p.

5.5.1 Libp2p ping: application dependencies


From the command line, within the libp2p_ping directory, install the following NPM
packages: libp2p, es6, @libp2p/tcp, @chainsafe/libp2p-noise, @libp2p/mplex, and
@multiformats/multiaddr. These packages will be later referenced within the Libp2p ping
code. Installing them can be done using the following command syntax which includes a
space between the package names:

npm install libp2p es6 @libp2p/tcp @chainsafe/libp2p-noise @libp2p/mplex


@multiformats/multiaddr

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


91

Installing these NPM packages will place several dependency files within the project
folder. Because you are using Javascript, compiling is not required. This is important
because once these dependency files have been downloaded to your local hard drive, as
long as the licensing of the code allows it, they can be copy/pasted or moved with the
rest of your project code, to allow removing the NPM registry as a point of centralization
going forward. In the next section, you begin programming the ping send and receive
functions, which will reference the NPM packages and files that were just installed.

5.5.2 Sending messages using Libp2p


Defining the protocol that is to be used for a transport as well as the configuration to use
for encrypting the transport is done using the createLibp2p function, which takes a JSON
formatted configuration object as its argument.
For an instance of Libp2p to function, the minimum configuration that is required is
parameters for transports and connectionEncryption. Libp2p uses the term transports to
refer to its connections. For this ping example, you will also define a multiplexer using
the parameter streamMuxers. A multiplexer as defined earlier in the chapter, is used
here to allow multiple exchanges to share the single TCP transport connection.
Depending on the intended use of the Libp2p transport, additional parameters such as
peerDiscovery and connectionManager can also be included in this configuration object to
apply more control to how peer connections are made. These additional parameters are
covered later in this chapter, but are not required for this ping example.
Within the /libp2p_ping/ directory, open the libp2p_ping.js file and add the code from
listing 5.1. This code defines a new libp2pInstance object which is configured to use a
transport connection based on TCP by calling an imported tcp() function, as well as
imported noise() and mplex() functions for the connectionEncryption and streamMuxers
parameters respectively.

Listing 5.1. Defining an instance of Libp2p.

import { createLibp2p } from 'libp2p' #1


import { tcp } from '@libp2p/tcp'
import { noise } from '@chainsafe/libp2p-noise'
import { mplex } from '@libp2p/mplex'

const libp2pInstance = await createLibp2p({ #2


transports: [tcp()],
connectionEncryption: [noise()],
streamMuxers: [mplex()]
})

#1 Importing files from the installed NPM dependencies


#2 Libp2p configuration object.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


92

The next step in configuring Libp2p to send ping messages is to let it know which address
it should use to communicate. As in the previous section, this is done by including it as
part of the configuration object. Libp2p can be set to use a single address or multiple.
Libp2p addresses are defined using the following format:

/<ip version>/<ip address>/<protocol>/<port>

If you are happy to use any available port, this can be achieved by setting the port
number to 0. It is also important to note that the protocol that is defined in the address
must match one of the available transports that have been set. In the previous example,
the transport is was defined as TCP. Listing 5.2 shows the updated configuration with a
new Libp2p address defined.

Listing 5.2. Setting a Libp2p communication address.

import { createLibp2p } from 'libp2p'


import { tcp } from '@libp2p/tcp'
import { noise } from '@chainsafe/libp2p-noise'
import { mplex } from '@libp2p/mplex'

const libp2pInstance = await createLibp2p({


addresses: {
listen: ['/ip4/127.0.0.1/tcp/0'] #1
},
transports: [tcp()],
connectionEncryption: [noise()],
streamMuxers: [mplex()]
})

#1 Libp2p communication address

Libp2p is now ready to be run and should be started using a new async function named
startLibp2p. An async function allows Javascript to continue processing a script, without
waiting for the function to complete. Because Libp2p typically will run as long as your
Web3 application is open, you do not want Libp2p to prevent other portions of Javascript
from running within your Web3 application. This is a change from the direct function calls
you used in previous chapters. After creating a startLibp2p async function, within the
function, Libp2p can be started using the .start() method on the Libp2p instance.
Querying the address that Libp2p uses is done using the getMultiaddrs() method on the
Libp2p instance. The completed startLibp2p async function, which both starts Libp2p and
prints the Libp2p address as a console log, is shown in listing 5.3.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


93

Listing 5.3. Starting Libp2p using an async function.

import { createLibp2p } from 'libp2p'


import { tcp } from '@libp2p/tcp'
import { noise } from '@chainsafe/libp2p-noise'
import { mplex } from '@libp2p/mplex'
import { multiaddr } from '@multiformats/multiaddr'

const startLibp2p = async () => { #1


const libp2pInstance = await createLibp2p({
addresses: {
listen: ['/ip4/127.0.0.1/tcp/0']
},
transports: [tcp()],
connectionEncryption: [noise()],
streamMuxers: [mplex()]
})

await libp2pInstance.start() #2
console.log('Libp2p is listening on addresses:')
libp2pInstance.getMultiaddrs().forEach((addr) => {
console.log(addr.toString()) #3
})
}

startLibp2p()

#1 Creating the startLibp2p function


#2 Starting Libp2p
#3 String conversion of Libp2p addresses

If you have not done so already, take a moment to update your libp2p_ping.js file so
that it matches the code shown in listing 5.3. You can test that your new configuration is
Libp2p works as expected by running your Javascript file using Node.js:

node libp2p_ping.js

When starting Libp2p for the first time, a unique encryption key is automatically
generated to encrypt Libp2ps' connections. As part of negotiating connections with other
Libp2p instances, that unique key is also used to derive a unique peer ID, which Libp2p
uses in conjunction with the address you have set. If your instance of Libp2p starts
successfully, the address that is returned by the getMultiaddrs() method will be in the
format of the following:

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


94

/<ip version>/<ip address>/<protocol>/<port>/p2p/<derived peer id>

In total, your output should match listing 5.4.

Listing 5.4. Libp2p console output.

listening on addresses:
/ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhuGHfd... #1

#1 Output of the getMultiaddrs() method

NOTE For more information about the processes and encryption behind how Libp2p generates
peer IDs, visit the Libp2p peer ID specification page at github.com/​libp2p/specs/​blob/master/​
peer-ids/​peer-ids.​m d.

After validating your Libp2p instance is able to start and listen for network connections,
you can add the second half of the code, which will send messages between two
instances of Libp2p running no your local machine.

5.5.3 Receiving messages using Libp2p


To demonstrate running two instances of Libp2p on your local machine, the Node.js
process module will be used. By using the process module, Node.js will automatically
allocate an isolated process on your local machine for each execution of your Javascript
code file (libp2p_ping.js). This means that if 0 was chosen as the listening port number in
the the prior section, each execution of libp2p_ping.js will grab a new available TCP port
for communications, while also generating a new and unique Libp2p peer ID address.
Enable the Node.js process module by adding the additional import statement shown in
listing 5.5.

Listing 5.5. Allowing multiple executions of libp2p_ping.js.

import process from 'node:process' #1

#1 Importing the Node.js process module

With the additional import statement included, you can now add additional programming
to allow your Libp2p instance to receive messages from peers. This additional
programming is being added into the same file so that a single execution of
libp2p_ping.js creates an instance of Libp2p with the capabilities to both send and
receive.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


95

NOTE Javascript as a programming language can run entirely within a web browser. Node.js on
the other hand is a server-side runtime for Javascript that runs outside a browser. By running
outside the browser, Node.js can access operating resources in ways not allowed from the
browser sandbox. In this section, Node.js is being used only for the purpose of running multiple
instances of the libp2p_ping.js file and is not required otherwise. Removing the node:process
module import statement would allow Code Radio and other similarly designed Web3
applications to run wholly within the browser, without external dependencies such as a Node.js
installation.

In the current implementation, the instance of Libp2p referenced within libp2p_ping.js


can only see itself, but cannot access any other instance of Libp2p that might have been
spawn from previous executions of libp2p_ping.js. To get around this, the logic that
handles the Libp2p ping uses an if statement that takes a Libp2p address as an input, as
shown in the following example:

node libp2p_ping.js /ip4/127.0.0.1/tcp/51324/p2p/KJghjkhgJhgJ...

When Node.js executes libp2p_ping.js in this example, it is reading the command as


three inputs, with the node keyword being counted as the first input. Because Javascript
starts its indexes at zero rather than one, this gives the Libp2p address and index
position of two. Add the if statement shown in listing 5.6 to the bottom of your
libp2p_ping.js file to allow your instance of Libp2p to initiate a ping command using the
input address as the destination. As a fallback, if no address is provided, the if statement
instead skips the ping.

Listing 5.6. Pinging a Libp2p address.

if (process.argv.length >= 3) {
const ma = multiaddr(process.argv[2]) #1
console.log(`pinging remote peer at ${process.argv[2]}`)
const latency = await libp2pInstance.ping(ma) #2
console.log(`pinged ${process.argv[2]} in ${latency}ms`)
} else { #3
console.log('no remote peer address given, skipping ping')
}

#1 Reading the Libp2p address


#2 Storing the ping result
#3 Skipping the ping when no address is provided

The additional if statement is all that is required to complete two-way ping


communications using Libp2p. If you have not done so, be sure to save your
libp2p_ping.js file before proceeding to test the new ping functionality in the next section.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


96

5.5.4 Testing your Libp2p code


To begin testing, make sure that any previously running instances of libp2p_ping.js have
been terminated. Starting from zero, there is not yet a running instance of Libp2p, and
therefore, no address to include as an input. Start your test by executing the
libp2p_ping.js file without an address input, as shown in the following example:

node libp2p_ping.js

The initial execution of the code, although not communicating with another instance of
Libp2p yet, prints its peer ID to the console, as seen earlier. Once arriving to the if
statement that was added in the previous section, the new Libp2p instance skips the ping
action as shown in listing 5.7. The Libp2p instance continues to operate and listen on it
programmed TCP port, event though there is no explicit action it has been programmed
to perform.

Listing 5.7. Console outputs printed by Libp2p.

listening on addresses:
/ip4/127.0.0.1/tcp/51324/p2p/KJghjkhgJhgJ... #1
no remote peer address given, skipping ping

#1 The cryptographically generated Libp2p peer id

In a new terminal window, the Libp2p peer ID tat was printed to the console from the
first instance, can now be used when launching the second instance, as shown in the
following example:

node libp2p_ping.js /ip4/127.0.0.1/tcp/51324/p2p/KJghjkhgJhgJ...

Launching the additional instance of libp2p_ping.js with the peer ID will allow the second
instance to initiate a network ping request to the peer ID that has been provided in order
to print a latency value to the console as shown in listing 5.8

Listing 5.8. Successful ping communication.

libp2p has started


listening on addresses: #1
/ip4/127.0.0.1/tcp/51325/p2p/QmYZirEPREz9vSRF...
pinging remote peer at /ip4/127.0.0.1/tcp/51324/p2p/KJghjkhgJhgJ...
pinged /ip4/127.0.0.1/tcp/51324/p2p/KJghjkhgJhgJ... in 3ms #2

#1 Printing the new Libp2p peer id


#2 Printing the performance result after 2-way communication

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


97

You have successfully instantiated two Libp2p nodes with the ability to send network
communication to each other. In the next section this working peer-to-peer
communication will be extended and implemented within Code Radio to allow sharing of
its music files peer-to-peer. If you have not terminated them, it is safe to leave your
active instances of Libp2p running, as the peer ids that have been output in this section
will be used later in this chapter when updating Code Radio.

5.6 Code Radio application architecture using Libp2p


A number of architectural changes have been made to the Code Radio application to
move it to a Web3 architecture. This is a good point to stop and review the changes that
have been made.
Reproduced from chapter 2, figure 5.5 shows the cloud-hosted centralized
architecture that was taken as the starting point for Code Radio at the beginning of this
book. This legacy architecture splits the Code Radio application into three components
that are centrally managed. The first being domain name coderadio.stream, which is
held by a single domain registrar—Amazon in this case and provided by the
infrastructure of Amazon Route 53. Next is the application compute, which delivers the
Code Radio HTML, CSS, and Javascript as a static site with Amazon Cloudfront. Finally,
there is the application storage, which holds the core music files played by the Code
Radio application, served by Amazon S3.

Figure 5.5. A Code Radio Web2 application architecture using centrally-managed infrastructure within
Amazon AWS.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


98

As you progressed through subsequent chapters, centralized compute provided by


Amazon Cloudfront has been replaced with client-side Javascript in chapter 3. Next
centralized storage provided by Amazon S3 was replaced with client-side storage using
localStorage and IndexedDB in chapter 4. In this chapter, the final pilar of the application
architecture, the domain hosting, is being replaced by Libp2p, which enables connectivity
peer-to-peer using its cryptographically unique peer ID in place of a centralized domain
registration provided by Amazon Route 53. This substitution is visualized in figure 5.6.

Figure 5.6. For Web3, infrastructure that previously lived within a centrally-managed cloud provider, can
now be replaced with Javascript programming that runs client-side.

As a final stop in showing the progression of Code Radios' Web3 architecture, figure 5.7
shows a revision of a second diagram that was first presented in chapter 2, to reflect the
new peer-to-peer nature of the application, in which every participant running the
application also serves as a portion of its infrastructure.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


99

Figure 5.7. Building an application that can run entirely client-side is a good foundation for adding in
interactions which operate peer-to-peer.

The following sections will now detail the programming of that final step of replacing
Code Radios' centralized domain registration with connectivity supported by peer ids
provided through Libp2p.

5.6.1 Starting a new network using seed lists and Libp2p bootstrap
Now that you have demonstrated working communication by manually connecting
instances of Libp2p, it is now time to configure Libp2p for automatic peer discovery. This
automatic peer discovery is the configuration that will be used with Code Radio. If you
have terminated the previously running instances of libp2p_ping.js from earlier in this
chapter, take a moment to again run two of those instances, so that they print their
chosen peer ID to the local console.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


100

With two instances of Libp2p running from using the libp2p_ping.js file, Code Radio
can be updated to use the bootstrap module of Libp2p. A detailed explanation of the role
of bootstrapping in Web3 systems is outlined in chapter 2 of this book, but as a review,
bootstrapping refers to the process of bringing a peer-to-peer network online for the first
time. When a Web3 application is first launched, there are not yet any peers to connect
to. In this unique scenario, a bootstrap configuration provides a list of peers to connect
to when the application starts. This initial list of peers is also sometimes referred to as a
seed list. In his section you will practice the bootstrap configuration and seed list concept
by applying it to Code Radio.
To add the bootstrap configuration to Code Radio, start by creating a new file named
libp2p_node.js (available within this books code repository at directory
/chapter_5/chapter_5_begin/code_radio). At the top of this new Javascript file, the
dependencies must be added as import statements. In addition to previously used import
statements, a new dependency for a bootstrap module is included. Add the important
statements shown in listing 5.9 to the top of your libp2p_node.js file. Again, note that the
Node.js process" module is being imported to allow running more than one instance of
Libp2p on your local machine.

Listing 5.9. Import statements for file libp2p_node.js.

import process from 'node:process' #1


import { createLibp2p } from 'libp2p'
import { tcp } from '@libp2p/tcp'
import { noise } from '@chainsafe/libp2p-noise'
import { mplex } from '@libp2p/mplex'
import { bootstrap } from '@libp2p/bootstrap' #2

#1 Importing the Node.js process module


#2 Importing the Libp2p bootstrap module

With import statements for all dependencies in place, the remainder of the Libp2p
instance configuration reuses portions of the code that you have seen in the Libp2p ping
example with slight modifications. The first modification is the inclusion of a new list of
Libp2p peer ids(seed list) that the application can use to communicate. These addresses
are defined as an array variable named bootstrapMultiaddrs. Take a moment to add this
new array variable to your libp2p_node.js file. Within the array, add the Libp2p peer ids
that were printed to the console earlier in the chapter when running your libp2p_ping.js
files. Note that these earlier instances should still be running on your local machine to
enable communication. An example of the completed bootstrapMultiaddrs array variable
can be seen in listing 5.10.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


101

Listing 5.10. Adding a list of Libp2p peer ids to Code Radio for application bootstrapping.

const bootstrapMultiaddrs = [ #1
'/ip4/127.0.0.1/tcp/50924/p2p/12D3KooWJ3zFDFr...',
'/ip4/127.0.0.1/tcp/61884/p2p/12D3KooWHdzWnJp...'
]

#1 An arbitrary number of peer ids can be stored in your array

The next step is to define the configuration for the instance of Libp2p that will be used by
Code Radio. As with the Libp2p ping example, you can do this by assigning the
libp2pInstance variable to the object being returned by the createLibp2p function. Within
this call to createLibp2p, you will include configurations for the listening address, which
will be your local machine while testing, as well as the transport, encryption, and
multiplexer configurations. A completed version of the Libp2p instance configuration is
shown in listing 5.11. For simplicity, the instance variable is named libp2pInstance. Take
a moment to add this portion of code to your Javascript file libp2p_node.js.

Listing 5.11. Creating the instance of Libp2p to be use dby Code Radio.

const libp2pInstance = await createLibp2p({ #1


addresses: {
listen: ['/ip4/127.0.0.1/tcp/0']
},
transports: [tcp()], <1> // can also be updated to use [webSockets()]
connectionEncryption: [noise()],
streamMuxers: [mplex()]
})

#1 Configuration parameters of the Libp2p instance

To make use of the bootstrap functionality to automatically connect to Libp2p peers, the
variable that is creating an instance of Libp2p will also need a few lines of additional
configuration that was not included in the earlier ping example. These additional
configuration lines do two things:
1. The first is to tell the instance of Libp2p to use the list of peer ids you
defined earlier within the bootstrapMultiaddrs variable; this is done
using a new peerDiscovery parameter.
2. The second is to configure Libp2p to autodial or attempt to
automatically connect to this list of peers using a new
connectionManager parameter.

You can see these additional parameters in listing 5.12. Before proceeding, take a
moment to add the additional configuration to your Javascript file, libp2p_node.js.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


102

Listing 5.12. Adding additional configuration to enable peer discovery.

const libp2pInstance = await createLibp2p({


addresses: {
listen: ['/ip4/127.0.0.1/tcp/0']
},
transports: [tcp()],
connectionEncryption: [noise()],
streamMuxers: [mplex()],
peerDiscovery: [
bootstrap({
list: bootstrapMultiaddrs, #1
})
],
connectionManager: {
autoDial: true, #2
}
})

#1 Libp2p can now make use of your defined seed list


#2 Code Radio now automatically connects to new peers

Once more, as done in the Libp2p ping example, now add the portion of Javascript code
which starts your instance of Libp2p and prints its unique peer ID value to the console as
shown in listing 5.13.

Listing 5.13. Starting your bootstrapped instance of Libp2p.

await libp2pInstance.start() #1
console.log('libp2p has started')

console.log('listening on addresses:')
libp2pInstance.getMultiaddrs().forEach((addr) => {
console.log(addr.toString()) #1
})

#1 Printing your unique peer id

The following step is not required for your instance of Libp2p to function, but is added to
assist in debugging your Libp2p instance in the event that an error occurs. Take a
moment to add two Javascript event listeners to your libp2p_node.js file. These event
listeners will listen for peer:discovery and peer:connect events and log these details to
the console. The completed configuration for this logging is shown in listing 5.14.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


103

Listing 5.14. Logging peer connections from Libp2p.

libp2pInstance.addEventListener('peer:discovery', (evt) => { #1


console.log('Discovered %s',
evt.detail.id.toString()) #1
})

libp2pInstance.connectionManager.addEventListener('peer:connect', (evt) => {


console.log('Connected to %s',
evt.detail.remotePeer.toString()) #2
})

#1 Printing learned peer ids to the console


#2 Printing the peer ID of connected peers

Your libp2p_node.js files now has all functionality required to start, discover new peers,
and automatically connect. When Libp2p discovers a peer, it saves this information
locally in something called a peer store . As part of keeping this peer store up-to-date,
the Libp2p protocol checks its connections to its peers periodically to ensure the peer is
still available. Also as part of these recurring checks, instances of Libp2p also notify
peers when new peers are discovered.
This means that as new application instances connect, they discover further peers
beyond those originally defined in the bootstrap seed list. In the case of Code Radio,
once you have programmed the configuration as described in this chapter and a few
additional users run the application, it becomes possible to fully remove the original peer
ids that are included in the seed list. This removal can be done without any impact on the
connectivity for the users who are already running the application, provided they have
learned about each other. New users who have never run the Code Radio application
however, would still be reliant on the initial seed list. The following section demonstrates
testing the seed list and peer discovery process.

5.6.2 Testing the Code Radio bootstrap process


With your Libp2p instances still running from the Libp2p ping exercise from earlier in the
chapter, you are ready to start additional instances of Libp2p using your newly created
Javascript file libp2p_node.js.
Because the Libp2p configuration parameters includes seed list of peer ids, including a
peer ID as an argument when running your code is no longer required. To begin your
test, from, within your working directory
/chapter_5/chapter_5_begin/code_radio/libp2p_node.js, start a new instance of Libp2p:

node libp2p_node.js

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


104

As with previous instances, your Javascript code will run and print its new peer ID to the
console as shown in listing 5.15.

Listing 5.15. Libp2p peer ID output.

listening on addresses:
/ip4/127.0.0.1/tcp/50438/p2p/8sgE2jqq9G... #1

#1 Output of the getMultiaddrs() method

Shortly after Libp2p generates its peer ID and begins listening on an open TCP port, it
progresses to reading from the configured seed list and attempts to establish peer
connections. If successful, these items trigger the respective Javascript event listeners to
print to the console the peer addresses it has learned, followed by confirmations of
successfully connecting to these peers, as shown in listing 5.16.

Listing 5.16. Libp2p connection confirmations.

Discovered /ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhu... #1
Discovered /ip4/127.0.0.1/tcp/50439/p2p/3kjhHG2nk...

Connected to /ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhu... #2
Connected to /ip4/127.0.0.1/tcp/50439/p2p/3kjhHG2nk...

#1 Connections defined in the bootstrap seed list


#2 Confirmation of successful peer connections

Going one step further, with the existing Libp2p instances still running, start one more
instance of libp2p_node.js. As this instance comes online, it repeats the same sequence
seen with the first instance by initially printing its newly generated peer ID and then
progressing to read peer ids from the configured seed list before attempting to connect
to them. Once the additional instance has established connections to one or more other
instances of Libp2p, it will begin the previously described process of sharing information
from its peer store, to propagate information for all the connections that it knows to the
other instances of Libp2p it has connected to (figure 5.8).

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


105

Figure 5.8. Using libp2p, application instances learn of new participants automatically by asking their
peers periodically, who they have connections to. This helps information for discovering new participants
to spread among all users of the application.

As this process runs, your Libp2p instances will attempt to connect to the new peer ids
they learn, resulting in the updated Libp2p connections being printed to console as shown
in listing 5.17.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


106

Listing 5.17. Learning new Libp2p peer connections.

Discovered /ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhu... #1
Discovered /ip4/127.0.0.1/tcp/50439/p2p/3kjhHG2nk...

Connected to /ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhu...
Connected to /ip4/127.0.0.1/tcp/50439/p2p/3kjhHG2nk...

Discovered /ip4/127.0.0.1/tcp/50438/p2p/8sgE2jqq9G...

Connected to /ip4/127.0.0.1/tcp/50438/p2p/sKJHkljhu... #2
Connected to /ip4/127.0.0.1/tcp/50439/p2p/3kjhHG2nk...
Connected to /ip4/127.0.0.1/tcp/50438/p2p/8sgE2jqq9G...

#1 Initial peers remain defined in the seed list


#2 New connections are learned from other peers

All instances of Libp2p running from the libp2p_node.js file have now learned of each
other and will continue to share periodic updates until terminated. If one or more of your
Libp2p instances are terminated, you will see this reflected, as the printed console output
refreshes with updates. If your running instanced of Libp2p do not behave as shown in
the previous listing, take a final moment to compare your written code to the final
reference version of the code provided with this book’s code source files in the
repository directory /chapter_5/chapter_5_end/code_radio/libp2p_node.js.

5.7 Summary
Libp2p is a network protocol that allows applications to connect peer-to-
peer.
When using Libp2p, centralized application DNS names can be replaced
with Libp2p peer ids.
Libp2p instances can be placed into two categories: standalone and
browser.
Running Libp2p standalone supports the TCP and QUIP transport
protocols.
Running Libp2p in the browser allows support for the WebRTC,
WebSocket, and WebTransport transport protocols in conjunction with
TCP and QUIP.
Libp2p encrypts all network connections by default.
Multiplexing can allow multiple streams of communication to share a
single Libp2p connection.
The Node.js built-in node:process module can be used to run multiple
instances of Libp2p to test Web3 application communication.
Using the Libp2p bootstrap module allows Web3 applications to
automatically connect to as well as share the connection details of new
application participants.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


107

Libp2p bootstrap processes rely on an initial seed list to allow new


application instances to connect and receive further connection
updates.
Configuring Javascript event listeners for peer connection events can
assist in viewing application communication and troubleshooting.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


108

6
Using Libp2p for content
distribution

This chapter covers


Understanding peer-to-peer pub/sub systems
Using the publish and subscribe features of Libp2p to synchronize media

In this chapter, you’ll learn about the publish/subscribe (pub/sub) features of Libp2p.
Using publish and subscribe, Web3 application developers allow instances of their
application to send and receive messages to each other instead of communicating via a
central server. This functionality replaces a central server and allows applications to stay
up to date by synchronizing settings and code changes to deliver rich media and other
content to users as they connect. To demonstrate this, you will update the Code Radio
demo application to allow application instances to synchronize the music catalog to
discover songs not in their local library and download those songs from peers. Within the
Code Radio application architecture, Libp2p pub/sub communications are part of the base
networking layer (figure 6.1).

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


109

Figure 6.1 Defining the Web3 application architecture of the Code Radio application starts at the base
network layer.

6.1 Introduction to Pub/Sub messaging


Pub/sub is a mechanism that allows systems or peers to share information around
specific topics. To share data, peers send messages or publish data labeled with a topic.
On the opposite end, anyone interested in receiving messages on that topic can
subscribe or opt-in to receiving those messages (figure 6.2). In practice this system
works similar to a YouTube channel. In peer-to-peer context, Web3 systems have the
added benefit that they can be built to allow anyone to publish and subscribe to data on
shared topics.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


110

Figure 6.2 Sharing messages using pub/sub.

A pub/sub system that most people have interacted with is a chat room. Using the
example of a travel chat room, all participants in a travel chat room are subscribed to
the topic travel. Within the chat room, everyone can share or publish messages about
travel, which are then delivered to the room and everyone who has subscribed. By
publishing and subscribing in this way, everyone in the chat room can stay up-to-date.
Beyond the ability to share data between participants, using pub/sub mechanics in a
Web3 application can also have drawbacks. When using pub/sub to share data, some
design targets to keep in mind include the following:
Reliability: Can all messages reach all subscribers in a reasonable
amount of time? How does the application behave if a message is
received late or not at all?
Speed: Does your application rely on receiving messages to update
portions of the user interface? If pub/sub operations affect your
application interface, delays in messaging can cause the application to
appear unresponsive to the user.
Efficiency: How many messages does your application need to send,
and how often? Sending many messages at a high frequency, or
messages that are very large, may degrade performance when the
application is used over a slow connection.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


111

Resilience: Can users join and leave your application without causing
service issues? Would your application perform the same if a large
portion of users were periodically offline? As a networking layer, Libp2p
dynamically handles changes in peer connectivity. A best practice in
Web3 design, is to avoid using statically assigned resources that could
become unavailable as participants join and leave your applications
network .
Scale: How many subscribers are expected for each topic? One way to
allow your Web3 application to scale when using pub/sub mechanics is
to assign each application features to a dedicated topic.
Simplicity: Across all topics and messages, is your application still
reasonable to maintain and debug? For each application feature, has
pub/sub messaging been reduced to a bare minimum? How are
messages exchanged to reach all application states? Can you easily re-
create all messages in isolation to debug and resolve application bugs?

The following section provides additional detail for how Libp2p handles network
communication and by extension, pub/sub communication underneath.

6.2 Peer-to-peer pub/sub communication with Libp2p


Using pub/sub functionality provided by Libp2p is one way to enable the capability in
peer-to-peer context. In a legacy Web2 scenario, a centralized message queue such as
Apache Kafka or a centrally-managed distributed data store such as Redis can also
provide pub/sub capability. In each case, the pub/sub implementation and therefore how
each handles the previously mentioned design considerations will be different. To reason
about the previous design considerations when using Libp2p, it is important to know
more about how Libp2p handles network communication underneath.

6.2.1 Peering modes used by Libp2p


When connecting and communicating, Libp2p maintains two modes of peering: full-
message peering and metadata-only peering. By default, Libp2p uses full-message
peering and sends full messages between peers. This method of peering consumes more
data compared with metadata-only peering. To offset this, at the time of writing, Libp2p
sets a default target of 6 for the number of peer connections and refers to this target as
the peering degree or network degree . This means that even if Libp2p learns of large
amounts of new peers that are available for connection, it will only attempt to negotiate
and maintain full-message exchange with 6 of them. If any of these 6 peers go offline,
Libp2p then chooses a new peer to establish full-message exchange with, to maintain its
target peering degree.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


112

NOTE When using Libp2p, changing the peering degree higher or lower is one way to tune your
application’s performance. For example, setting the number of target connections to a higher
value can deliver higher increased reliability, because there is an increased chance that each
application instance will receive all possible messages. This change, however, also adds the
drawback of increasing the amount of data sent in the network, because all messages are being
sent to more peers. For the purposes of the Code Radio application presented in this book, the
peering degree is left at its default.

In addition to full-message peering, Libp2p also operates its metadata-only peering in


parallel. Libp2p uses metadata-only peering to maintain an awareness among peers that
are not directly connected for full-message sharing. When instances of Libp2p connect as
metadata-only peers, a smaller portion of data is exchanged to notify which messages
are available (without sending the actual message contents).
As Web3 applications using Libp2p are opened and closed, the number of full-message
peers that are connected can fluctuate. When the number of connected full-message
peers falls below the intended target, Libp2p automatically negotiates to promote an
available metadata-only peer to being a full-message peer. Libp2p refers to this checking
of peer connectivity as a heartbeat. The default interval of Libp2ps heartbeat interval is
every 1 second. These peering and heartbeat mechanics form the basic of the pub/sub
logic within Libp2p.

6.2.2 Libp2p pub/sub mechanics


Within Libp2p, full-message peers keep track of each others active subscriptions, even if
they are not subscribed to those topics themselves. Libp2p does this by exchanging
subscription information as part of status updates that are shared among full-message
peers. Doing this helps instances of Libp2p learn and keep track of new topics as they
become available and is also referred to as fan-out topics. Additionally, Libp2p also uses
subscription tracking to determine which available peers should be promoted to full-
message peering and which should operate as metadata-only peers. Libp2p peers that
do not have subscriptions in common are more likely to be demoted to metadata-only.
Figure 6.3 shows an example message flow for peer subscriptions.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


113

Figure 6.3 Once connected, full-message peers share a record of all of their active subscriptions.

When a Libp2p instance wants to publish a message for a given topic, it broadcasts that
message to all full-message peers it is connected to. This broadcast happens regardless
of whether the connected peer is subscribed to the topic and can make use of the
message. After full-message peers receive published messages, they in turn store a
copy of the message and broadcast that message to all of their connected full-message
peers. Through this process, messages that are published are able to propagate through
the entire network. Depending on how densely the network is connected, it is possible for
a peer to receive a published message more than once, from different full-message
peers along disjointed paths. When this happens, duplicates of already received
messages are discarded. Libp2p refers to this sharing and rebroadcasting of messages
as gossipsub.

NOTE Full detail for the JavaScript implementation of Libp2p gossipsub is available on Github at:
https://github.​com/ChainSafe/​js-libp2p-​gossipsub

In addition to sharing published messages with all full-message peers, Libp2p randomly
selects a portion of metadata-only peers to share metadata about the messages it has
received. At the time of writing the default number of random metadata-only peers that
are sent full messages is also 6 (the same as for full-message peers). If a message does
not arrive from a full-message peer as intended, it can be discovered through
communications with metadata-only peers.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


114

When missed messages are discovered through shared metadata, the full message is
then requested from the metadata-only peer. Message metadata sharing also happens
during the configured heartbeat interval, which by default is every 1 second.
Configuring your future Web3 applications to take advantage of Libp2p networking will
always be a balance of trade-offs. The following section provides a final high-level
summary of all data exchanged during Libp2p pub/sub operations which can be used as
a quick reference for modeling application data usage.

6.2.3 How Libp2p handles pub/sub state


The following is a list of records that instances of Libp2p maintain when participating in
sub/sub operations. Keep in mind that this state data does not account for the separate
overhead that is incurred as Libp2p peers establish and negotiate base-level peer
connections ahead of participating in pub/sub activities.
Subscriptions: A list of topics the Libp2p instance is subscribed to.
Fan-out topics: A list of topics where pub/sub messages have been
received, but the Libp2p instance is not subscribed to.
Connected peers: A list of all peers the Libp2 instance is connected to,
as well as their peering type—​full-message or metadata-only.
Recent messages: A cache of messages that have been recently
received. All messages are stored with a sender, a sequence number
for when they arrived, and the message contents.

Figure 6.4 shows this same pub/sub state data as a visual mapping.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


115

Figure 6.4 A summary of data maintained as part of Libp2ps pub/sub state. Source: https://docs.​
libp2p.io/​c oncepts/​pubsub/overview/​#state

In the following sections, you see Libp2p pub/sub in action through updating the code of
the Code Radio application to use the pub/sub features of Libp2p.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


116

6.3 Enabling Libp2p pub/sub within Code Radio


In the previous chapter, you were initially introduced to Libp2p by being guided through
the fundamentals of its networking layer and by writing the initial code that facilitated
instances of the Code Radio application to discover and establish connections with each
other. In isolation, being connected peer-to-peer does not do much. The sections that
follow build on the peer-to-peer connectivity to enable pub/sub messaging and
synchronization between instances of the Code Radio web3 application. If you do not
have a copy of the Git repository on your local machine, take a moment to clone the
code repository from https://github.​com/stevenplatt/​web3-application-​a rchitecture.​git.
If you have not completed the code practice of the previous chapter, take a moment
to review that chapter, because the code and explanations there provide an introduction
to the material presented in the sections that follow. The starting point for code in this
chapter is within the book’s Github repository directory: web3-application-
architecture/chapter_6/chapter_6_begin/js.
Before Libp2p pub/sub can be used within Code Radio, a few modules must be
installed. Primary among these are the Libp2p floodsub module, which enables support
for publishing and subscribing to topics, and the pubsub-peer-discovery module, which
enables the use of pub/sub messages to assist Libp2p in peer discovery. Additionally, a
number of dependency modules must also be installed. Using the --save argument will
store the installed modules within your package.json file. Start by opening a terminal
session within your projects current working /js directory, and install the modules using
the commands shown in listing 6.1

Listing 6.1 Installing Libp2p pubsub dependencies

code_radio % npm install @libp2p/floodsub \


@libp2p/pubsub-peer-discovery @nodeutils/defaults-deep \
execa fs-extra p-defer uint8arrays which --save

With required dependencies installed, pub/sub functionality can be used within Code
Radio. Using the existing libp2p_node.js JavaScript file created in the previous chapter,
two import statements must be added. The first is to import the floodsub module that
enables pub/sub functionality within Libp2p, the second is to import two functions,
pubsubListen and pubsubBraodcast, which are used to synchronize music data within
Code Radio. The two completed import statements are shown in listing 6.2. Take a
moment to add these to the top of your libp2p_node.js file. Later in the chapter, pub/sub
messages will be managed within these two functions.

Listing 6.2 Importing Libp2p pubsub dependencies

import { floodsub } from '@libp2p/floodsub'


import {pubsubListen, pubsubBroadcast} from './music_sync.js'

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


117

Because you are using the Libp2p instance created in the previous chapter, the only
update required is to enable pub/sub by telling Libp2p which protocol it should use. This
is done in the same format that was used to define transport, encryption, and other
protocols within the Libp2p instance. In this case, the protocol used for pub/sub
messages is floodsub. Add this protocol configuration to your Libp2p instance so that it
matches the example shown in listing 6.3.

Listing 6.3 Configuring pubsub to use the "floodsub" implementation

const node = await createLibp2p({


addresses: {listen: ['/ip4/127.0.0.1/tcp/0']},
transports: [tcp()],
connectionEncryption: [noise()],
streamMuxers: [mplex()],
peerDiscovery: [
bootstrap({list: bootstrapMultiaddrs})],
connectionManager: {autoDial: true},
pubsub: floodsub()}) #1

#1 Defined pub/sub protocol

At this stage, your Libp2p instance can start and make use of pub/sub functionality, but it
has not been configured to do anything with it. To get your instance of Libp2p to send
and receive messages, you are going to ask it to run the pubsubListen() and
pubsubBroadcast() functions that were imported. You will fully configure these functions
in the following sections. Both of these functions take your Libp2p object (which is named
node ) as an input, as shown in listing 6.4.

Listing 6.4 Invoking the pubsub listen and broadcast methods

pubsubListen(node) #1
console.log('synchronizing with the network...')

pubsubBroadcast(node) #1
console.log('broadcasting music to peers...')}

#1 Passing Libp2p instance named node

Add the two functions shown in listing 6.4 to your existing startLibp2p function, so that
Libp2p runs them each time it starts. Once added, your startLibp2p function should
match the updated version shown in listing 6.5.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


118

Listing 6.5 Complete configuration of your Libp2p node

const startLibp2p = async () => {


await node.start()
console.log('libp2p has started, listening on addresses:')
node.getMultiaddrs().forEach((addr) => {console.log(addr.toString())})

node.addEventListener('peer:discovery', (evt) => {


console.log('Discovered %s', evt.detail.id.toString())})

node.connectionManager.addEventListener('peer:connect', (evt) => {


console.log('Connected to %s', evt.detail.remotePeer.toString())})

pubsubListen(node) #1
console.log('synchronizing with the network...')

pubsubBroadcast(node) #1
console.log('broadcasting music to peers...')}

#1 Pub/sub functions

This completes the updates required for Code Radios Libp2p instance. When the Code
Radio application starts Libp2p, it also includes additional pub/sub functionality which it
uses by running the two functions pubsubListen() and pubsubBroadcast(). In the sections
that follow, you will build out the logic within these two functions to practice how Libp2p
pub/sub messaging can be used.

6.4 Using pub/sub to serve music peer-to-peer


In the context of Code Radio, instances of the application need a way to inform each
other which songs they have a copy of. Doing this opens the possibility of Code Radio
instances transferring songs between each other to stay up-to-date. It also allows for the
possibility of new users of the Code Radio application to start the application with only a
minimal set of music or no music and then synchronize on the first boot. Such a scenario
is useful to keep the application size to a minimum and bring the functionality of Code
Radio closer to that of a streaming music service such as Spotify, except running fully
peer-to-peer.

NOTE For this chapter, Code Radio’s music files are stored within the ./music_data/ directory
inside of three files: music_classic.js, music_ambient.js, and music_lofi.js.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


119

To configure pub/sub messaging sent by Code Radio, create a new JavaScript file named
music_sync.js within your current working directory, . This is the filename used with the
import statement used earlier in this chapter. Once completed, this new file holds the
application logic that handled synchronizing Code Radios music files. To do this, a few
additional import statements are required. In addition to the items that are imported and
used by the Libp2p node, the music_sync.js files need additional import statements that
define the location of Code Radios music files as well as import statements to allow
processing string values using the fromString and toString modules. Add these import
statements to the top of the new music_sync.js file, so that they match the example
shown in listing 6.6.

Listing 6.6 Importing dependencies for Libp2p music synchronization

import { classicSongs } from './music_data/music_classic.js' #1


import { ambientSongs } from './music_data/music_ambient.js'
import { lofiSongs } from './music_data/music_lofi.js'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' #2
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'

#1 Code Radio music files


#2 String processing for song names

To receive information on certain topics using pub/sub, the topics will need to be defined.
These topics can carry any arbitrary name. Topics can also be added or removed over
time. For Code Radio a topic is defined for each of its radio stations. To configure the
new Code Radio topics, start by creating new JavaScript variables named station_1,
station_2, and station_3 within the music_sync.js file. These new variables hold the Code
Radio station name, as shown in listing 6.7.

Listing 6.7 Defining Code Radio music stations

const station_1 = 'classic' #1


const station_2 = 'ambient'
const station_3 = 'lofi'

#1 Code Radio station name

With the Code Radio station names defined, they can now be used as topics with various
pub/sub functions. To organize pub/sub functions, create two new JavaScript functions
within the music_sync.js file, named pubsubListen() and pubsubBroadcast(), which take
a single input named node . The node input in this case refers to the Code Radio Libp2p
node.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


120

Recall that these are the two functions that were previously imported into the
libp2p_node.js file. For those import statements to work, the JavaScript export keyword
must also be included before the pubsubListen() and pubsubBroadcast() function names.
The following sections will build out Code Radio’s publish and subscribe functionality
using these two functions.

6.4.1 Defining Code Radio stations as subscriptions


Within the pubsubListen() function, defining the topics that Code Radio will be subscribed
to is done using node.pubsub.subscribe(). A total of three node.subpub.subscribe()
statements are needed in total, one for each of Code Radio’s music stations. Note that
.subscribe() takes only a single string input. For this reason, the three station topics
cannot be passed as a single array or other consolidated format. Take a moment to
update your music_sync.js file with the new pubsubListen() function so that it matches
the example shown in listing 6.8.

Listing 6.8 Creating subscribe functions to receive Code Radio song updates

export function pubsubListen(node){ #1


node.pubsub.subscribe(station_1)
node.pubsub.subscribe(station_2)
node.pubsub.subscribe(station_3)
}

#1 Subscribe logic

You now have Code Radio’s subscriptions defined and the application will receive all
messages that are labeled with these topics. What happens when those messages are
received is still not defined however. To trigger some action when messages are
received, you will use a pubsub event listener in addition to the subscription definitions.
The event listener is intended to allow Code Radio to parse the message that is sent
before saving it into memory. To parse the message reliably, you must also decide on a
format for the Libp2p pub/sub messages that are being sent. For this example, Code
Radio will send song names, along with the associated song data. The message format is
therefore defined as:

[song title]:::[song data]

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


121

NOTE The format chosen for Libp2p messages will be unique to each application and is part of its
information architecture (initially mentioned in chapter 2 of this book). Information architecture
is not unique for Web3 and is not covered in depth in this book. For more information on
information architecture, including different ways to organize information within an application,
see "Information architecture models and examples", available at: https://learn.​m icrosoft.​
com/en-us/​sharepoint/​information-​architecture-​m odels-examples

To parse messages in this format, a new object within the pub/sub event listener, named
trackTitle is required. This new object will act as a message consumer which takes the
message that is received and saves the song title that is placed at the beginning of it.
The song title is indicated using the delimiter ::: in this case. From this saved song title,
Code Radio can then compare the songs it already has using an if statement. If a song is
received that is not already in memory, Code Radio can then take both the song title and
the song data and save it to the local storage of its associated radio station. Recall that
the associated radio station is configured as the message topic. It is also here that music
objects such as classicSongs that were imported at the top of the music_sync.js file are
used for the first time. A completed version of this logic for station_1 is shown in listing
6.9

Listing 6.9 Listening for an event within Code Radios subscribe logic

export function pubsubListen(node){


node.pubsub.subscribe(station_1)
node.pubsub.subscribe(station_2)
node.pubsub.subscribe(station_3)
node.pubsub.addEventListener('message', (evt) => {
const trackTitle = uint8ArrayToString(evt.detail.data).split(":::") #1

if ( (evt.detail.topic == station_1) && (!(classicSongs[trackTitle[0]])) ) { #2


console.log(`"received song "${trackTitle[1]}" by ${trackTitle[0]}"`)
console.log(`"adding song to the ${station_1} station..."`)
classicSongs[trackTitle[0]] = trackTitle[1] #3
}
})
}

#1 trackTitle message consumer


#2 Checking for the received song within station_1
#3 Saving to the music object classicSongs

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


122

To perform a similar check for messages received for station_2 and station_3, the IF
conditional logic must be updated to include entries for the additional two stations. Take
a moment to update your pubsub event listener so that it matches the example shown in
listing 6.10.

Listing 6.10 Code Radio completed event listener logic

node.pubsub.addEventListener('message', (evt) => {


const trackTitle = uint8ArrayToString(evt.detail.data).split(":::")

if ( (evt.detail.topic == station_1) && (!(classicSongs[trackTitle[0]])) ) {


console.log(`"received song "${trackTitle[1]}" by ${trackTitle[0]}"`)
console.log(`"adding song to the ${station_1} station..."`)
classicSongs[trackTitle[0]] = trackTitle[1]
} else if ( (evt.detail.topic == station_2) && (!(ambientSongs[trackTitle[0]])) )
{ #1
console.log(`"received song "${trackTitle[1]}" by ${trackTitle[0]}"`)
console.log(`"adding song to the ${station_2} station..."`)
ambientSongs[trackTitle[0]] = trackTitle[1]
} else if ( (evt.detail.topic == station_3) && (!(lofiSongs[trackTitle[0]])) ) {
#2
console.log(`"received song "${trackTitle[1]}" by ${trackTitle[0]}"`)
console.log(`"adding song to the ${station_2} station..."`)
lofiSongs[trackTitle[0]] = trackTitle[1]
} else {
console.log(`received song is already in library, ignoring...`)
}
})

#1 Check for music using the topic station_2


#2 Check for music using the topic station_3

This completes the logic for Code Radio’s music subscriptions. At this point, Code Radio
is configured with three subscriptions, each corresponding to one of its three radio
stations. Code Radio now also includes pub/sub event listener logic that allows it to
perform actions on the messages received from the three subscriptions/stations. The
effect of this logic, when combined with peer discovery functions added in the previous
chapter, means that an instance of Code Radio that contains no music files can join the
network, discover other Code Radio users, and receive messages that contain music files
from other users, assuming they have been programmed to send such messages.
Programming Code Radio to broadcast the songs it has stored locally is covered in the
following section.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


123

6.4.2 Synchronizing music files using using message broadcasts


To allow Code Radio to send messages, and by extension, share music between
instances, a second function named pubsubBroadcast() must be added to the
music_sync.js file. This new function will be used later to trigger the pubsub.publish()
function for sending messages. Unlike the pubsubListen() function that is triggered by an
event listener anytime a new message arrives, the logic that sends messages must be
called explicitly. To do this, you will add the JavaScript built-in setInterval() function to
ensure that broadcast logic runs on a recurring schedule to share the songs that are in
each Code Radio instances' local library. A completed version of the pubsubBroadcast()
function triggered on a 5-second interval is shown in listing 6.11. Note that setInterval()
defines time using milliseconds.

Listing 6.11 Creating publish functions to share Code Radio music files

export function pubsubBroadcast(node){ #1


setInterval(() => {

}, 5000) #2
}

#1 Message broadcast function


#2 Broadcast interval

Now that the broadcast interval is set, you need to implement Code Radio to share the
list of songs it has during each broadcast interval. Starting again with station_1 as an
example, the songs for this station are stored within the classicSongs object that was
imported earlier. This object holds a dictionary of songs that use the song title as a key
and string-encoded music data as the value. At each interval, you should set Code Radio
to iterate over the items in the classicSongs object and broadcast them using the
pubsub.publish() function.
The pubsub.publish() function takes two inputs:
The subscription topic being broadcast to
The message

To share the music data along with the song, both the key and value are sent inside the
message field, delineated by the character string ":::". The message body is encoded as
an unsigned integer array before it is sent. This is done to separate the key, value, and
delimiter portions of the string so that it can be processed more easily by the receiver.
Listing 6.12 shows a completed version of this broadcast logic for the station_1 topic.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


124

Listing 6.12 Triggering message broadcasts within Code Radios publish logic

export function pubsubBroadcast(node){


setInterval(() => {
for (const [key, value] of Object.entries(classicSongs)) { #1
node.pubsub.publish(station_1,
uint8ArrayFromString(`${key}:::${value}`)).catch(err => { #2
console.error(err)})
}
}, 5000)
}

#1 Iterating over stored songs


#2 String to array processing

Extending this broadcast logic to the two additional stations can be done as shown in
listing 6.13. Take a moment to update the music_sync.js file so that it includes the
additional logic to broadcast all three stations music files.

Listing 6.13 Completed Code Radio publish logic

1 export function pubsubBroadcast(node){


2 setInterval(() => {
3 for (const [key, value] of Object.entries(classicSongs)) {
4 node.pubsub.publish(station_1,
5 uint8ArrayFromString(`${key}:::${value}`)).catch(err => {
6 console.error(err)})
7 }
8
9 for (const [key, value] of Object.entries(ambientSongs)) { #1
10 node.pubsub.publish(station_2,
11 uint8ArrayFromString(`${key}:::${value}`)).catch(err => {
12 console.error(err)})
13 }
14
15 for (const [key, value] of Object.entries(lofiSongs)) { #2
16 node.pubsub.publish(station_2,
17 uint8ArrayFromString(`${key}:::${value}`)).catch(err => {
18 console.error(err)})
19 }
20 }, 5000)
21 }

#1 ambientSongs broadcast
#2 lofiSongs broadcast

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


125

The programming needed to allow Code Radio to broadcast the music files it has stored
locally is now complete. Using Libp2p, the Code Radio application now has the ability to
discover new users, connect to new users, broadcast songs that are stored locally, as
well as receive songs that have been broadcast by others. Within Code Radios'
application architecture, all of this functionality happens within Libp2p and at the
applications' base network layer. In the next chapter, the storage layer of Code Radios'
application architecture will be built. Building on top of the network layer that you have
now completed, Code radio will be configured to persist song data client-side, so that it is
available even after the Code Radio application has been closed and restarted.

6.5 Summary
Libp2p can be used to enable message exchanges at the network layer
of Web3 applications.
Pub/sub or publish and subscribe mechanics can be used to allow Web3
applications to share a single state by sending update and status
messages peer-to-peer.
The JavaScript implementation of Libp2p has a native pub/sub module
that can send messages using the floodsub protocol.
When designing Web3 systems that rely on pub/sub communications,
some design targets to keep in mind are reliability, speed, efficiency,
resilience, scale, and simplicity.
Changing the quantity and size of pub/sub messages can affect
application design targets, such as efficiency and scale.
Pub/sub organizes messages by topic.
An application must subscribe to a topic to ensure all messages for that
topic are received.
Anyone can send pub/sub messages to a topic, even if they are not
subscribed to that topic.
Libp2p exchanges two types of messages with peers: full messages and
metadata-only messages.
Libp2p maintains a pub/sub state which includes a list of its
subscriptions, a list of its peers with their subscriptions and peering
type, a history of messages sent to topics that are not subscribed to,
and finally, a list of messages that have been received.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


126

Appendix B. Encoding music


files for IndexedDB storage

On both the Mac and Linux platforms, the commands required to perform a base64
conversion of the wav music file is natively supported. On Windows platforms, a third-
party tool such as base64.exe can be downloaded to perform the conversion. On a Mac,
the conversion can be performed from the terminal using the following format:

base64 -i INPUT_FILE -o OUTPUT_FILE

On a Linux machine, line wrapping can cause unexpected behavior in later file
processing. To perform the conversion on a Linux machine without line wrapping, the
command becomes:

base64 INPUT_FILE -w 0 > OUTPUT_FILE

Last, from a Windows platform machine, the command to complete the file conversion
will depend on the third-party tool used. When using base64.exe on the Windows
platform, the command becomes:

Base64.exe -e INPUT_FILE > OUTPUT_FILE

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


127

Back within the new indexedDB.js file, this base64-encoded music file can be stored as a
Javascript object. With base64 encoding, the wav file is made a generic string
representation, and because this string will be converted a Javascript blob object next, it
must also include a MIME type as part of the variable assignment. MIME stands for
Multipurpose Internet Mail Extensions, but are used more broadly today to indicate the
intended document structure or format for data that is otherwise made generic (as done
with base64 encoding). Take a moment to add a new variable named "tranAndInk" to
your indexedDB.js file, using the following example; note that the string value that is
received from base64 encoding the music file in the previous step is included here:

var trapAndInk = "data:audio/wav;base64,<your base64-encoded string>";

After assigning this variable, it can be referenced and converted into a binary blob
format for use with IndexedDB. The code snippet shown in listing B.1 is published by the
engineer Boris Smus and is available on Github at: gist.github.​com/borismus/​1032746.
This code snippet takes the base64-encoded variable as an input and returns an
immutable, fixed-length array buffer to be used as a binary blob. Take a moment to add
this code snippet to your indexedDB.js file.

Listing B.1. Converting the base64-encoded music file into an array.

function convertDataURIToBinary(dataURI) {
var BASE64_MARKER = ';base64,';
var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
var base64 = dataURI.substring(base64Index);
var raw = window.atob(base64);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for(i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}

The next step is to store the array that is returned as a binary blob. This is done in two
steps. As referenced earlier, Javascript blob objects require a MIME type. The first step is
to declare the MIME type when storing the array as a blob. The second step is to
generate a URL or Universal Resource Locator for the new blob object. This URL is what
you will use to address and reference the blob object and is created using the
URL.createObjectURL() static method that is part of the URL API. The two steps can be
programmed using the code shown in listing B.2. Take a moment to add these additional
variable assignments to your indexedDB.js file.

© Manning Publications Co. To comment go to liveBook

Licensed to Dragan Golubovic <info@metrocarta.com>


128

NOTE The additional URL that is created is a requirement inherited from the Media Source
specification that handles media playback. More information about the URL.createObjectURL()
static method can be found as part of Mozilla Developer Network documentation at: developer.​
mozilla.org/​en-US/docs/​Web/API/URL/​createObjectURL.

Listing B.2. Storing the music file array as a binary blob with a URL address.

var binary = convertDataURIToBinary(trapAndInk);


var blob = new Blob([binary], {type : 'audio/wav'});
var blobUrl = URL.createObjectURL(blob); #1

#1 Blob URL location

You now have an IndexedDB compatible audio file, with an addressable URL paired with
it. The next step is a bit of housekeeping to declare an initial audio source as shown in
listing B.3. Take a moment to add this code to your indexedDB.js file.

Listing B.3. Declaring an initial audio source for Code Radio.

const audioURL = document.createElement("audio");


audioURL.src = blobUrl;

The manual encoding of your Code Radio music files are completed at this stage and are
ready to be stored into IndexedDB.

© Manning Publications Co. To comment go to liveBook

You might also like