Chapter 4. OpenBazaar
This chapter will take a deep dive into the decentralized market, OpenBazaar. We’ll discuss the rationale and overall structure of the transactions it supports and then talk through implementing an OpenBazaar instance as well as its potential next steps and flaws.
Why Make OpenBazaar?
Bitcoin really got people excited about developing next-generation ecommerce with speedy micropayments and better security. The first institutions to utilize Bitcoin at a large scale were centralized providers like Overstock and Dish Network. Bitcoin provided these mainstream companies with an opportunity to showcase their tech savvy, but its pseudonymity and immediate value transfer were much more suited to a marketplace for illegal goods: Silk Road.
Silk Road was like the underground version of eBay. It was a centralized website, but to gain access to it you had to use onion routing via Tor. The creator made it very difficult for casual users to access it, and it was considered the pinnacle of the “dark web.” People primarily bought and sold illegal drugs on Silk Road, particularly in jurisdictions with strict antidrug laws. Although part of Silk Road’s business might have been universally repugnant, other sales involved cross-jurisdictional sales of “light” drugs like marijuana (and even tobacco) or nondrug items like erotic art and books, jewelry, and the like.
It all worked out for quite a while, but eventually the United States government caught up with Silk Road because it had a central point of failure: all of its data was stored on a single server. So, when the government seized the server, Silk Road was taken down and all of the users lost their data associated with the website. Another user tried restarting the site as Silk Road 2.0; eventually the feds caught up with him as well, and the site was shut down for a second time.
Pirate Bay is in a similar position; however, given the relatively less serious level of illegality it enables, the government response has been commensurately less permanent. The site has been taken down multiple times by multiple government organizations, and yet it just keeps popping up. The site owners simply plan on the fly where they next want their server to be after it’s been taken down. Clearly this isn’t a long-term solution, but there is a very high-value need for people to get things that are otherwise impossible for them because of the law.
Aside from the technical vulnerability, the other point of failure for marketplace apps has been the leader in charge with access to all the data. The arrest of Ross Ulbricht, aka the Dread Pirate Roberts, the founder of Silk Road, made headlines around the world. He is now serving life in prison for narcotics trafficking and computer crimes. The need for a decentralized marketplace became more and more apparent after the government’s dealings with Silk Road—a marketplace where no single person had administrative access control over the data and that could run locally on anyone’s computer. It was out of this need that OpenBazaar was born.
What Is OpenBazaar?
With OpenBazaar, there is no central server involved at all. It’s a peer-to-peer client to which no government entity can restrict access. OpenBazaar doesn’t operate under the approval of any law; it’s the evolution of unrestricted global marketplace. As its creators put it,“It’s like eBay and BitTorrent had a baby.”
OpenBazaar is a platform that lets buyers and sellers connect directly to sell their goods without involving a third party to host the data and charge a transaction fee. The creators wanted to build on the idea of creating a truly free trade platform for people to send and receive goods without having to go through a central authority. The Internet has never really hosted anything like the bazaars of the past: peer-to-peer marketplaces where buyers and sellers could interact directly with one another without anyone between observing the transaction. OpenBazaar hopes to bring that concept to the Internet.
The developers won a hackathon in Toronto for their project called DarkMarket. It has since been renamed OpenBazaar and they have many developers on the team now. They mostly receive funds in the form of donations from people. They don’t really profit from this, and that’s one major flaw in this dapp: without incentivizing network members, this business plan doesn’t scale. The flaw could be mitigated by introducing a metacoin that would increase in value instead of using Bitcoin directly.
How Does OpenBazaar Work?
Everyone in the OpenBazaar network is a node in the P2P network. Everyone is assigned three roles that they can build on: merchant, buyer, and/or arbiter. You can choose what role you mainly want to build your reputation for, and you are not limited to one role. The currency presently in use is Bitcoin, removing the barrier to entry of having to deal with a novel currency—but this also doesn’t let the developers automatically be paid for their work. Let’s talk about what the process looks like for each of these three types of actors in the network.
Merchant
OpenBazaar’s interface is still under development, but all of the basic elements necessary for the site to function in alpha are in place. Merchants only need to go to the setting tab and type a nickname for their store. They also input their profile image, Bitcoin address, and Namecoin ID (optional). After they’ve filled out their credentials (see Figure 4-1), they can save it, with the data being saved locally to their computers.
Merchants also have the ability to communicate with their buyers, either directly on OpenBazaar using a messaging protocol built on ZeroMQ or by using a third-party communication protocol like email, bitmessage, or their own website. Because OpenBazaar is currently in alpha phase, updates to the protocol can delete a merchant’s store data. As a result, the developers created a backup option that lets merchants create a backup of their store data that they can easily reintegrate in case of data loss.
The interesting part comes when merchants list their goods on OpenBazaar. It uses the concept of Ricardian contracts to facilitate trade on the network. This is different from smart contracts because they don’t live on a blockchain; instead, they live on the merchant’s computer. A Ricardian contract is basically a way to track the liability of Party A when selling goods to Party B. It represents a single unit of a good. These contracts are used in the dapp to track legitimately signed agreements between both parties, and these agreements can’t be forged after the contract has been signed (Figure 4-2).
So the contract on the frontend looks like a simple input where you enter details about your product and price. Additionally, it links your product to your Bitcoin address and GUID, the buyer’s Bitcoin address and GUID, and a third-party notary that you both deem trustworthy.
When a buyer actually makes a purchase, the seller will receive a notification that her product has an order pending. The buyer will receive details about the notary that the seller has trusted to hold the funds. The buyer can choose to trust the notary. If he declines, the notary will send the funds back to him. If the seller does choose to trust the notary, she can send the buyer the product. If the buyer receives the item, he instructs the notary to send the funds to the seller. If the buyer doesn’t do this, the notary will act as a dispute resolution party, and after compiling information from both sides, will decide as to which party is most likely telling the truth.
Buyer
Buyers type in their credentials in the same fashion as the merchant, but they have the duty of selecting a notary. Buyers select the notary, but it’s the sellers who can accept or decline the validity and reputation of said notary. As of this writing, this dapp is in its early days, and building trust and reputation requires time, so it’s best for people to only make small transactions in case notaries are bad actors. Eventually quality notaries will rise to the top—perhaps people will create notary services as businesses and they will become the most-trusted and dominant players.
Notary
Anyone can be a notary by simply turning on a checkbox in their profile. Whenever a buyer adds a party as a notary for their purchase contract, the notary can receive funds, mitigate disputes, and send funds to the rightful party. Notaries can charge a percentage fee for providing dispute resolution. If both selling and buying parties complete their transaction without needing the assistance of the notary, there is no payment necessary. If the notary is needed to refund the buyer or engage in dispute resolution, the notary will receive a percentage from the multisig. Notaries publicly display their fee under the services tab in their “storefront.”
Currently, notaries automatically accept all transactions that assign them as a notary, but eventually they’ll be able to screen transactions and have the ability to accept or decline them. We can see in Figure 4-3 that the basic functionality is there. Figure 4-4 shows a completed order.
How to Install OpenBazaar
Now that I’ve talked about what OpenBazaar is and how it works, let’s download it and give it a whirl ourselves. We’ll talk about it from a technical standpoint, look at the pros and cons of the technologies the developers decided to use in the stack, and discuss the way they’ve decided to design this dapp.
As of this writing, there is no binary here, so you need to build from source.
First you’re going to need to install Python. If you’re running the latest version of OS X, it comes with Python 2.7 installed out of the box. Otherwise, you’ll need to install it manually via Homebrew. Homebrew is like the missing “apt-get” feature from Linux for OS X. I’ve found it supremely useful when compiling dapps from source, because there is almost always at least one missing-dependency error.
To install Homebrew, in the Terminal, type the following:
ruby
-
e
"$(curl -fsSL
https
:
//
raw
.
githubusercontent
.
com
/
Homebrew
/
install
/
master
/
install
Homebrew
/
install
/
master
/
in
From then on, you can easily install thousands of packages by using this format:
brew
install
________
OpenBazaar was built using Python. It’s a solid object-oriented language that has amassed a huge number of useful libraries over the years, with many distributed projects like RPyc being built with it, so using it is not a bad choice at all.
You’re also going to need to install Pip, Python’s module installer:
Brew
install
pip
Now, you can build OpenBazaar from source:
git
clone
https
:
//
github
.
com
/
OpenBazaar
/
OpenBazaar
.
git
cd
OpenBazaar
./
configure
.
sh
./
OpenBazaar
start
Possible Errors
The following is a collection of errors you might encounter as you build this code locally on your own machine. Keep in mind that this is still actively in development, so things might break.
Dependencies
You might get some dependency errors, given that this project is under development. Don’t be surprised to receive messages similar to “X wasn’t found.” You can just systematically install each dependency manually using Homebrew as they come up. So if you get an error, locate the dependency, Brew-install it, and then retry ./OpenBazaar start
. A new dependency error might come up. Just keep repeating until all the necessary dependencies are installed and then it will run.
Ports
You might see the following error:
1.
If
you
are
using
VPN
,
configure
port
forwarding
or
disable
your
VPN
temporarily
2.
Configure
your
router
to
forward
traffic
from
port
62112
for
both
TCP
and
UDP
to
your
local
port
62112
This means that one of the ports that OpenBazaar is trying to use is blocked by either a firewall or your router. Ensure that those ports can be accessed in your system and router settings.
Data Storage and Retrieval
Data isn’t stored on a DHT in OpenBazaar; it’s stored locally in a SQLite datastore at each node. In datastore.py, we can see the set_item
method that takes a key-value pair as input with some timestamps and credentials. It inserts the pair into the database as its own entry locally on the user’s computer. Here’s the code in OpenBazaar that does that:
def
set_item
(
self
,
key
,
value
,
last_published
,
originally_published
,
original_publisher_id
,
market_id
=
1
):
rows
=
self
.
db_connection
.
select_entries
(
"datastore"
,
{
"key"
:
key
,
"market_id"
:
market_id
}
)
if
len
(
rows
)
==
0
:
self
.
db_connection
.
insert_entry
(
"datastore"
,
{
'key'
:
key
,
'value'
:
value
,
'lastPublished'
:
last_published
,
'originallyPublished'
:
originally_published
,
'originalPublisherID'
:
original_publisher_id
,
'market_id'
:
market_id
}
)
else
:
self
.
db_connection
.
update_entries
(
"datastore"
,
{
'key'
:
key
,
'value'
:
value
,
'lastPublished'
:
last_published
,
'originallyPublished'
:
originally_published
,
'originalPublisherID'
:
original_publisher_id
,
'market_id'
:
market_id
},
{
'key'
:
key
,
'market_id'
:
market_id
}
)
You can then query these values by using the db_query
method:
def
_db_query
(
self
,
key
,
column_name
):
row
=
self
.
db_connection
.
select_entries
(
"datastore"
,
{
"key"
:
key
})
if
len
(
row
)
!=
0
:
value
=
row
[
0
][
column_name
]
try
:
value
=
ast
.
literal_eval
(
value
)
except
Exception
:
pass
return
value
This only needs a key to retrieve the necessary value as well as the column name—which doubles as the publisher_id
.
OpenBazaar does use a DHT, but not for data storage. The DHT in OpenBazaar was inspired by Kademlia (like BitTorrent and IPFS) and is used as a sort of “yellow pages” for peers. It’s a decentralized index of peers that instructs every node how to contact every other node for the sake of selling and sharing Ricardian contracts. When two nodes connect via the DHT each node can pull data from the other directly:
def
__init__
(
self
,
market_id
,
key
,
call
=
"findNode"
,
callback
=
None
):
self
.
key
=
key
# Key to search for
self
.
call
=
call
# Either findNode or findValue depending on search
self
.
callback
=
callback
# Callback for when search finishes
self
.
shortlist
=
[]
# List of nodes that are being searched against
self
.
active_probes
=
[]
#
self
.
already_contacted
=
[]
# Nodes are added to this list when they've been sent a findXXX action
self
.
previous_closest_node
=
None
# This is updated to be the closest node found during search
self
.
find_value_result
=
{}
# If a find_value search is found this is the value
self
.
slow_node_count
=
[
0
]
#
self
.
contacted_now
=
0
# Counter for how many nodes have been contacted
self
.
prev_shortlist_length
=
0
self
.
log
=
logging
.
getLogger
(
'[
%s
]
%s
'
%
(
market_id
,
self
.
__class__
.
__name__
)
)
# Create a unique ID (SHA1) for this iterative_find request to support
parallel
searches
self
.
find_id
=
hashlib
.
sha1
(
os
.
urandom
(
128
))
.
hexdigest
()
In the file node/DHT.py, under the class DHTSearch
, the init method helps search through the DHT for other nodes about which you want to get more data. It assumes that you know the key of the node so that lookup time is faster, but in OpenBazaar’s DHT, it acts like a broadcasts for every node so brute-force discovery is also possible.
OpenBazaar’s designers have structured all relevant user data to be sent as a JSON object called data
in proto_page
under protocol.py:
def
proto_page
(
uri
,
pubkey
,
guid
,
text
,
signature
,
nickname
,
PGPPubKey
,
,
bitmessage
,
arbiter
,
notary
,
notary_description
,
notary_fee
,
arbiter_description
,
sin
,
homepage
,
avatar_url
):
data
=
{
'type'
:
'page'
,
'uri'
:
uri
,
'pubkey'
:
pubkey
,
'senderGUID'
:
guid
,
'text'
:
text
,
'nickname'
:
nickname
,
'PGPPubKey'
:
PGPPubKey
,
'email'
:
,
'bitmessage'
:
bitmessage
,
'arbiter'
:
arbiter
,
'notary'
:
notary
,
'notary_description'
:
notary_description
,
'notary_fee'
:
notary_fee
,
'arbiter_description'
:
arbiter_description
,
'sin'
:
sin
,
'homepage'
:
homepage
,
'avatar_url'
:
avatar_url
,
'v'
:
constants
.
VERSION
}
return
data
This data is sent and retrieved between users identifying one another after they have been found in the DHT.
Another great thing about the DHT is that we can also search by keyword. Because keywords are user-defined in their storefronts, they can be product related or category related, making search easier and more user-friendly:
def
find_listings_by_keyword
(
self
,
keyword
,
listing_filter
=
None
,
callback
=
None
):
hashvalue
=
hashlib
.
new
(
'ripemd160'
)
keyword_key
=
'keyword-
%s
'
%
keyword
hashvalue
.
update
(
keyword_key
.
encode
(
'utf-8'
))
listing_index_key
=
hashvalue
.
hexdigest
()
self
.
log
.
info
(
'Finding contracts for keyword:
%s
'
,
keyword
)
self
.
iterative_find_value
(
listing_index_key
,
callback
)
Like IPFS, OpenBazaar uses a DHT. Unlike IPFS, OpenBazaar doesn’t accommodate data replication via content-addressed data; no matter how many people want it, the data will only live locally on the originating computer. As is logical for a system that doesn’t rely on distributing copies of the data, there also is no versioning of the data built in.
Identity
Nodes in OpenBazaar have their own unique GUID. This is similar to IPFS nodes with their peerIDs:
Under
node
/
transport
.
py
def
_generate_new_keypair
(
self
):
seed
=
str
(
random
.
randrange
(
2
**
256
))
# Move to BIP32 keys m/0/0/0
wallet
=
bitcoin
.
bip32_ckd
(
bitcoin
.
bip32_master_key
(
seed
),
0
)
wallet_chain
=
bitcoin
.
bip32_ckd
(
wallet
,
0
)
bip32_identity_priv
=
bitcoin
.
bip32_ckd
(
wallet_chain
,
0
)
identity_priv
=
bitcoin
.
bip32_extract_key
(
bip32_identity_priv
)
bip32_identity_pub
=
bitcoin
.
bip32_privtopub
(
bip32_identity_priv
)
identity_pub
=
bitcoin
.
encode_pubkey
(
bitcoin
.
bip32_extract_key
(
bip32_identity_pub
),
'hex'
)
self
.
pubkey
=
identity_pub
self
.
secret
=
identity_priv
# Generate SIN
sha_hash
=
hashlib
.
sha256
()
sha_hash
.
update
(
self
.
pubkey
)
ripe_hash
=
hashlib
.
new
(
'ripemd160'
)
ripe_hash
.
update
(
sha_hash
.
digest
())
self
.
guid
=
ripe_hash
.
hexdigest
()
These identities are generated via Bitcoin’s BIP32 (hierarchical deterministic wallets) protocol by generating a new SIN using SHA-256 to create your GUID. The GUID is as unique as each Bitcoin address is unique so we don’t have to worry about duplicates.
So, we’re able to give unique identities to people, just like in IPFS, by using the elliptic-curve technology behind Bitcoin, but how do we make them human-readable? OpenBazaar also has an input credential for your Namecoin ID as an additional identity field besides your self-assigned nickname. Thus, people basically can have duplicate nicknames and the GUID can be used to validate which one is which. This is suboptimal: perhaps you can memorize the last five digits of someone’s GUID as well as their username. But Namecoin makes up for this flaw by making it possible for users to also have a Namecoin ID:
def
is_valid_Namecoin
(
Namecoin
,
guid
):
if
not
Namecoin
or
not
guid
:
return
False
server
=
DNSChainServer
.
Server
(
constants
.
DNSCHAIN_SERVER_IP
,
""
)
_log
.
info
(
"Looking up Namecoin id:
%s
"
,
Namecoin
)
try
:
data
=
server
.
lookup
(
"id/"
+
Namecoin
)
except
(
DNSChainServer
.
DataNotFound
,
DNSChainServer
.
MalformedJSON
):
_log
.
info
(
'Claimed remote Namecoin id not found:
%s
'
,
Namecoin
)
return
False
return
data
.
get
(
'OpenBazaar'
)
==
guid
The code checks against DNSChain to see if the Namecoin ID is valid every time by checking whether that GUID is stored in the Namecoin address of the person claiming the identity. DNSChain is a hybrid DNS server for easy access to Namecoin data via an API.
So OpenBazaar’s identity problem is solved through a combination of uniquely generated GUID’s and Namecoin, similar to IPFS.
Reputation
But what about reputation? Reputation is a big part of any marketplace environment; buyers want to be able to trust sellers, and vice versa. In a centralized model, server-owners can hand out reputation to individuals and, with appropriate security, they don’t need to deal with individuals tampering with their own reputation to defraud the system. In a decentralized system, reputation is much more difficult to verify.
Trust is dealt with in OpenBazaar through two different types of synergistic systems: global trust and projected trust. When all members of the network trust a particular user of the network in the same way, this is called global trust. This trust is established through proof-of-burn and proof-of-timelock. Projected trust is trust directed toward a certain node, which might be different for each user of the network. So the trust is projected from each user to the node. This trust is established through a pseudonymous partial knowledge web of trust.
Let’s look at each of these methods in more detail.
Method 1: proof-of-burn
When a seller creates a store, he must spend Bitcoin that is lost and never returns. This makes it expensive for a user to create multiple identities and is the fundamental Sybil-attack prevention mechanism in OpenBazaar. Though not perfect, it is a deterrent. The larger the proof-of-burn, the more expensive it is to make an account, but the higher the barrier to entry for potential players is to use the service. Publicly and verifiably burning some coins in a set-limit currency is remurrage on the remainder. Remurrage is the opposite of demurrage (the cost of holding currency over a given period). Suppose that you are sitting at home on your laptop and create a currency with 10 million coins that people begin trading instantly. When you go out for a walk and come back, there are now only 5 million issued coins that aren’t burned. If you’re holding any of that currency, it’s the equivalent of receiving a dividend on top of the general economy-tracking price that we’ve grown to expect from a set-quantity currency like Bitcoin.
The dapp first generates a burn address directly from a node’s GUID:
def
burnaddr_from_guid
(
guid_hex
):
_log
.
debug
(
"burnaddr_from_guid:
%s
"
,
guid_hex
)
prefix
=
'6f'
if
TESTNET
else
'00'
guid_full_hex
=
prefix
+
guid_hex
_log
.
debug
(
"GUID address on bitcoin net:
%s
"
,
guid_full_hex
)
# Perturbate GUID to ensure unspendability through
# near-collision resistance of SHA256 by flipping
# the last non-checksum bit of the address.
guid_full
=
guid_full_hex
.
decode
(
'hex'
)
guid_prt
=
guid_full
[:
-
1
]
+
chr
(
ord
(
guid_full
[
-
1
])
^
1
)
addr_prt
=
obelisk
.
bitcoin
.
EncodeBase58Check
(
guid_prt
)
_log
.
debug
(
"Perturbated bitcoin proof-of-burn address:
%s
"
,
addr_prt
)
return
addr_prt
From there it’s just a simple transaction on the GUID. All nodes can verify that a certain GUID has burned coins (proof-of-burn) by performing the same burn
add_from_guid
function on the guid hex and verifying its burn amount on the blockchain.
Method 2: proof-of-timelock
Proof-of-burn lets the network create identities such that it is costly to recreate. Proof-of-timelock, by rendering a particular amount of coin unspendable for a time (and tying a user identity to that unspent coin as a “deposit”), ensures that it’s impossible that a huge number of real-world identities associated with one real-world entity can simultaneously exist at any specific moment in time. Proof-of-timelock is not as strong an insurance as proof-of-burn: proof-of-burn is, effectively, permanent proof-of-timelock.
In proof-of-timelock, the node that wants to establish trust toward a pseudonymous identity must provably lock a specified amount of currency inside a transaction that gives the currency back to them eventually. The transaction has the feature that it isn’t executed for a specified amount of time. The network knows that the transaction will take place eventually, the amount of it, and the amount of time it will remain locked. All of these things are publicly verifiable.
One of the main appeals of proof-of-timelock is psychological: it just feels less guilty than proof-of-burn. There is a certain psychological burden associated with money destruction and it might not be an easy one to overcome. People will most likely use it more often than proof-of-burn.
The Bitcoin blockchain currently doesn’t allow for a proof-of-timelock mechanism directly. Although the Bitcoin protocol supports the nLockTime
value, the mechanism is not currently honored by running nodes. This means that the transaction won’t be broadcast in a publicly verifiable way.
This would be a perfect use case for the Ethereum blockchain because it allows for Turing-complete smart contracts. OpenBazaar decided to avoid using it though because it hasn’t proven feasible in practice and has a lot of problems in terms of scalability and performance. This was a smart decision and the sidechain proposal will mitigate some of this risk eventually.
Method 3: trust-as-risk (most viable)
The developers are still working out the details of the web-of-trust model and how to actually implement it programmatically, but it seems like they are heading in the direction of using trust-as-risk. They’ve been toying with the idea of letting people extend others a line of credit if the creditor trusts the party to whom it is extending it. So the idea would be that if you really trust someone, you can give them 0.1 Bitcoin in a line of credit via a multisignature transaction; if you stop trusting them, you can withdraw your line of credit.
The indicators for trust need to be hosted persistently in a decentralized way. Because the OpenBazaar developers don’t want to add to blockchain bloat on Bitcoin (always a good train of thought), they are leaning toward using Namecoin as a good alternative. I find this to be a really reasonable approach.
They could end up not even implementing a web-of-trust because it might not even matter that much. In real life, when someone tries to scam us and we lose money, we can just call our banks to cancel a trade and recover the funds. In the OpenBazaar network, a notary is the key intermediary in transactions and has the potential to prevent a scam from occurring the first place. Nodes could just place all of their trust with them rather than other peers. It will be interesting to see how trust plays out in the network, but a web-of-trust to me seems like a necessary addition to secure the network.
What Could OpenBazaar Have Done Better?
To begin with, the most important flaw here is the lack of an internal currency. Bitcoin provides immediate liquidity, and that is good for sellers, but having an internal currency is a win-win situation for early adopters and the developers themselves. First of all, using OpenBazaar is a risk for early adopters anyway, given that their Bitcoin can be stolen because of the lack of reputable notaries and reputation takes time to build. It would be better if OpenBazaar issued its own currency that would be used to make purchases inside the dapp. OpenBazaar would have a crowd sale and set an initial price of the currency and set a limited number of tokens.
These coins would be colored coins, so they could set up a Bitcoin contract address (aka send-coins-here) which would calculate how many OBcoins you get in return to send to the OpenBazaar address you specify. As OpenBazaar grows in valuation, the values of the coins would rise. OpenBazaaar early adopters would be rewarded for their efforts in bootstrapping the network despite the risk, liquidity would increase for buyers and sellers, and, most important, the developers of the open source software would be paid for their work. Funding is one of the major competitive advantages of centralized closed-source software over open source. The former simply is able to pay top developers to maintain and upgrade the app, but with an internal currency, we can bring that model to open source software.
One other questionable choice is OpenBazaar’s data storage model: just storing it in a local SQLite datastore with no redundancy or replication. If they used IPFS for data storage, it would be more resilient. The more people that visited a store, the more copies of that store’s data there would be. Store owners could get a notice of how many other people were replicating their encrypted store data for peace of mind.
The knowledge of how OpenBazaar was built (and the constraints it was built under) with its flaws and successes can play into the design of many types of apps. We’ll see some of the same themes pop up in our next case study: Lighthouse.
Get Decentralized Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.