Technology Dry Goods | Zero Knowledge Proof Learn by Coding: An Introduction to libsnark
I hope that through this series of articles, all developers can get started and practice in person, quickly get started with libsnark in a short time, understand the basic concepts of libsnark step by step, learn how to develop zk-SNARKs circuits, complete the generation and verification of proofs, and finally have zero knowledge Proofs apply to real business.
1.Introduction to zk-SNARKs and libsnark
Zero-knowledge proof is probably the most promising cryptographic black technology at present. And zk-SNARKs is the abbreviation of a type of zero-knowledge proof scheme, which is called Zero-Knowledge Succinct Non-interactive Arguments of Knowledge. This name contains almost all of its technical characteristics, that is, it can prove the correctness of a proposition without revealing any other information, and the resulting certificate is concise (Succinct), which means that the resulting certificate is small enough , And has nothing to do with the amount of calculation, it is a constant. In other words, you can theoretically prove something to everyone without exposing any privacy, and the generated proof is small in size and low in verification cost, and has nothing to do with the amount of content calculation that needs to be proven. It sounds so wonderful!
zk-SNARKs can be applied to many scenarios, such as privacy protection, blockchain expansion, verifiable computing, etc. This article does not introduce the theoretical details of zk-SNARKS and zero-knowledge proofs. Students who are not familiar with or want to learn more can read other articles or papers.
For example, three famous blog posts about zk-SNARKs by Vitalik.
- The main theme of foreign media reports in 2019: cryptocurrencies will not "die"
- Suzhou released 30 new industrial policies, including the construction of fintech supervision sandbox (full text)
- Bitcoin network UTXO reaches 64.5 million, continues to grow and hits record high
-
https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649 -
https://medium.com/@VitalikButerin/exploring-elliptic-curve-pairings-c73c1864e627 -
https://medium.com/@VitalikButerin/zk-snarks-under-the-hood-b33151a013f6
Of course, you are also welcome to pay attention to the series of "Exploring Zero Knowledge Proof" and "Learning from scratch by zk-SNARK" series by Abe Labs, and find more information from the "Zero Knowledge Proof Learning Resources Summary" maintained by Abe Labs.
- "Zk-SNARKs in-depth explanation of zero-knowledge proofs": https://www.yuque.com/u428635/scg32w/edmn74
- "Talking about Zero-knowledge Proof 2: Short Non-Interaction Proof (SNARK)": https://mp.weixin.qq.com/s/623bceLkCjgtFHB6W3D0oA
- "Explore Zero Knowledge Proof" series: https://sec-bit.github.io/blog/2019/07/31/zero-knowledge-and-proof/
- "Learn zk-SNARK from scratch" series: https://sec-bit.github.io/blog/2019/12/25/learn-zk-snark-from-zero-part-one/
- "Zero-knowledge proof learning resource summary": https://sec-bit.github.io/blog/2019/11/07/zkp-learning-resources/
-
[GGPR13] Quadratic span programs and succinct NIZKs without PCPs, Rosario Gennaro, Craig Gentry, Bryan Parno, Mariana Raykova, EUROCRYPT 2013 -
[PGHR13] Pinocchio: Nearly Practical Verifiable Computation, Bryan Parno, Craig Gentry, Jon Howell, Mariana Raykova, IEEE Symposium on Security and Privacy (Oakland) 2013 -
[BCGTV13] SNARKs for C: Verifying Program Executions Succinctly and in Zero Knowledge, Eli Ben-Sasson, Alessandro Chiesa, Daniel Genkin, Eran Tromer, Madars Virza, CRYPTO 2013 -
[BCIOP13] Succinct non-interactive arguments via linear interactive Proofs, Nir Bitansky, Alessandro Chiesa, Yuval Ishai, Rafail Ostrovsky, Omer Paneth, Theory of Cryptography Conference 2013 -
[BCTV14a] Succinct non-interactive zero knowledge for a von Neumann architecture, Eli Ben-Sasson, Alessandro Chiesa, Eran Tromer, Madars Virza, USENIX Security 2014 -
[BCTV14b] Scalable succinct non-interactive arguments via cycles of elliptic curves, Eli Ben-Sasson, Alessandro Chiesa, Eran Tromer, Madars Virza, CRYPTO 2014 -
[Groth16] On the Size of Pairing-based Non-interactive Arguments, Jens Groth, EUROCRYPT 2016
The solid theoretical foundation and engineering ability allow the authors of libsnark to reduce complexity, implement the advanced theories and complex formulas as shown in the following figure, and abstract a simple interface highly engineered for the convenience of developers. Pay tribute to these pioneers who extended extraordinary theoretical research to larger scale applications.
The figure below is an overview of the modules of libsnark, taken from the doctoral dissertation of Madars Virza, the first author of libsnark code contribution (https://madars.org/phd-thesis/).
The libsnark framework provides multiple implementations of the general proof system, of which the more commonly used are BCTV14a and Groth16.
Looking at the path of libsnark / libsnark / zk_proof_systems , we can find the specific implementation of various certification systems by libsnark, and they are classified according to different categories, and specific papers on which the implementation is based are also attached.
among them:
-
zk_proof_systems / ppzksnark / r1cs_ppzksnark corresponds to BCTV14a -
zk_proof_systems / ppzksnark / r1cs_gg_ppzksnark corresponds to Groth16
2. Basic principles and steps
Using the libsnark library to develop zk-SNARKs applications can be briefly summarized in principle as the following four steps:
-
Express the proposition to be proved as R1CS (Rank One Constraint System) -
Generate a common parameter for the proposition using a generation algorithm ( G ) -
Proof of R1CS Satisfaction Using Proof Algorithm ( P ) -
Use verification algorithm ( V ) to verify proof
https://media.consensys.net/introduction-to-zksnarks-with-examples-3283b554fc3b
There is such a function C (x, out), which is used to determine whether the secret x satisfies the equation x ^ 3 + x + 5 == out, and returns true if it is satisfied.
function C (x, out) {
return (x ^ 3 + x + 5 == out);
}
lambda <-random ()
(pk, vk) = G (C, lambda)
proof = P (pk, out, x)
V (vk, out, proof)? = True
3.Set up zk-SNARKs application development environment
Let's get into the hands-on section, quickly get started with libsnark, and run through the examples.
Download the libsnark minimum code library libsnark_abc corresponding to this article first.
git clone https://github.com/sec-bit/libsnark_abc.git
Pull the libsnark code through the git submodule.
cd libsnark_abc
git submodule update --init --recursive
sudo apt-get install build-essential cmake git libgmp3-dev libprocps4-dev python-markdown libboost-all-dev libssl-dev
mkdir build && cd build && cmake ..
mkdir build && cd build && CPPFLAGS = -I / usr / local / opt / openssl / include LDFLAGS = -L / usr / local / opt / openssl / lib PKG_CONFIG_PATH = / usr / local / opt / openssl / lib / pkgconfig cmake- DWITH_PROCPS = OFF -DWITH_SUPERCOP = OFF ..
make
main
range
test
./src/main
Eventually, the following log shows that everything is normal. You have successfully owned the zkSNARK application development environment and successfully ran the first demo of zk-SNARKs .
4.Understand the sample code
Let's go through the code carefully. The sample project contains 3 codes (see also the appendix at the end of the article).
There are only dozens of lines of code, of which run_r1cs_gg_ppzksnark () is the main part. It is easy to see that the actual code that really works is only the following 5 lines.
r1cs_gg_ppzksnark_keypair <ppT> keypair = r1cs_gg_ppzksnark_generator <ppT> (example.constraint_system);
r1cs_gg_ppzksnark_processed_verification_key <ppT> pvk = r1cs_gg_ppzksnark_verifier_process_vk <ppT> (keypair.vk);
r1cs_gg_ppzksnark_proof <ppT> proof = r1cs_gg_ppzksnark_prover <ppT> (keypair.pk, example.primary_input, example.auxiliary_input);
const bool ans = r1cs_gg_ppzksnark_verifier_strong_IC <ppT> (keypair.vk, example.primary_input, proof);
const bool ans2 = r1cs_gg_ppzksnark_online_verifier_strong_IC <ppT> (pvk, example.primary_input, proof);
Just from the "extra long" function name, you can see what each step is doing, but you can't see the details of how to construct the circuit. In fact, it just calls the built-in r1cs_example, which hides the implementation details.
That being the case, let's learn the details of the circuit with a more intuitive example . Study src / test.cpp, this example is adapted from Christian Lundkvist's libsnark-tutorial (https://github.com/christianlundkvist/libsnark-tutorial).
Only three header files are referenced at the beginning of the code:
#include <libsnark / common / default_types / r1cs_gg_ppzksnark_pp.hpp>
#include <libsnark / zk_proof_systems / ppzksnark / r1cs_gg_ppzksnark / r1cs_gg_ppzksnark.hpp>
#include <libsnark / gadgetlib1 / pb_variable.hpp>
As mentioned earlier, r1cs_gg_ppzksnark corresponds to the Groth16 scheme. Gg is added here to distinguish r1cs_ppzksnark (that is, the BCTV14a solution), which stands for Generic Group Model. Groth16 security proof relies on the Generic Group Model, in exchange for stronger security assumptions for better performance and shorter proofs.
The first header file is to introduce the default_r1cs_gg_ppzksnark_pp type, and the second header file is to introduce various interfaces related to proof. pb_variable is used to define circuit-related variables.
Next you need to perform some initialization, define the finite fields used, and initialize the curve parameters. This is equivalent to each preparation.
typedef libff :: Fr <default_r1cs_gg_ppzksnark_pp> FieldT;
default_r1cs_gg_ppzksnark_pp :: init_public_params ();
Next, we need to clarify what the "proposition to prove" is. Here we may follow the previous example and prove that the secret x satisfies the equation x ^ 3 + x + 5 == out. This is actually the example used in the Vitalik blog post "Quadratic Arithmetic Programs: from Zero to Hero" (https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649). If you are new to the changes below, try reading this blog post.
By introducing the intermediate variables sym_1, y, and sym_2, flatten x ^ 3 + x + 5 = out into several quadratic equations, some of which only involve simple multiplication or addition. Corresponding to arithmetic circuits are multiplication gates and additions. Method . You can easily draw the corresponding circuit on paper.
x * x = sym_1
sym_1 * x = y
y + x = sym_2
sym_2 + 5 = out
// Create protoboard
protoboard <FieldT> pb;
// Define variables
pb_variable <FieldT> x;
pb_variable <FieldT> sym_1;
pb_variable <FieldT> y;
pb_variable <FieldT> sym_2;
pb_variable <FieldT> out;
out.allocate (pb, "out");
x.allocate (pb, "x");
sym_1.allocate (pb, "sym_1");
y.allocate (pb, "y");
sym_2.allocate (pb, "sym_2");
pb.set_input_sizes (1);
// x * x = sym_1
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (x, x, sym_1));
// sym_1 * x = y
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (sym_1, x, y));
// y + x = sym_2
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (y + x, 1, sym_2));
// sym_2 + 5 = ~ out
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (sym_2 + 5, 1, out));
const r1cs_constraint_system <FieldT> constraint_system = pb.get_constraint_system ();
const r1cs_gg_ppzksnark_keypair <default_r1cs_gg_ppzksnark_pp> keypair = r1cs_gg_ppzksnark_generator <default_r1cs_gg_ppzksnark_pp> (constraint_system);
pb.val (out) = 35;
pb.val (x) = 3;
pb.val (sym_1) = 9;
pb.val (y) = 27;
pb.val (sym_2) = 30;
const r1cs_gg_ppzksnark_proof <default_r1cs_gg_ppzksnark_pp> proof = r1cs_gg_ppzksnark_prover <default_r1cs_gg_ppzksnark_pp> (keypair.pk, pb.primary_input ()); pb.auxiliary_input ());
bool verified = r1cs_gg_ppzksnark_verifier_strong_IC <default_r1cs_gg_ppzksnark_pp> (keypair.vk, pb.primary_input (), proof);
With just a few dozen lines of code, you can easily manipulate the latest research results of academic zk-SNARKs.
5, get started again
After the above example, we have learned all the important steps for developing zk-SNARKs circuits using the libsnark library.
Now let's consolidate with a new example: Prove that the number is less than 60 without revealing the size of the secret number .
How should this be done in a regular program with an operator under libsnark?
The main workload and difficulty of zk-SNARKs circuit development lies in how to accurately describe all constraints in the proposition with code. Once the description is not "precise", either the constraint is missed or the constraint is written incorrectly, and the content that the final circuit wants to prove will be far from the original proposition. The example in the previous section only involved simple multiplication and addition, which is consistent with the most basic form of r1cs_constraint, so the expression of constraints is relatively easy. In addition, almost all constraints are not very intuitive, and it is difficult for a beginner to describe the constraint details correctly.
Fortunately, libsnark has implemented a lot of basic circuit widgets for us. There are many gadgets available directly under gadgetlib1 and gadgetlib2. Among them, gadgetlib1 is more commonly used, which includes hash calculation, merkle tree, pairing and other circuit implementations including sha256.
DangDangDang , comparisonlib in gadgetlib1 / gadgets / basic_gadgets.hpp is exactly what we need.
comparison_gadget (protoboard <FieldT> & pb,
const size_t n,
const pb_linear_combination <FieldT> & A,
const pb_linear_combination <FieldT> & B,
const pb_variable <FieldT> & less,
const pb_variable <FieldT> & less_or_eq,
const std :: string & annotation_prefix = "")
protoboard <FieldT> pb;
pb_variable <FieldT> x, max;
pb_variable <FieldT> less, less_or_eq;
x.allocate (pb, "x");
max.allocate (pb, "max");
pb.val (max) = 60;
comparison_gadget <FieldT> cmp (pb, 10, x, max, less, less_or_eq, "cmp");
cmp.generate_r1cs_constraints ();
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (less, 1, FieldT :: one ()));
// Add witness values
pb.val (x) = 18; // secret
cmp.generate_r1cs_witness ();
6.What's NEXT?
After reading this, I believe that everyone has a preliminary understanding of the use of libsnark and zk-SNARKs circuit development.
You may have found that the use of libsnark is relatively simple, and the real focus is on zk-SNARKs circuit development. As mentioned earlier, all constraints in the proposition to be proved must be described with the code "exactly". "Missing" or "mistyped" constraints can make the content of the proof very different from the original intention, which makes the proof meaningless.
How to correctly and efficiently translate real business logic into zk-SNARKs circuit code is exactly what our developers need to continue to study and practice.
Fortunately, we already have a libsnark proving ground, and we can easily modify and add code to try it out.
No matter how complicated the circuit is, it is formed by simpler "circuit components" combined packaging. Therefore, the basic library that comes with libsnark is a very important learning material-you must learn both how to use them and study the implementation principles.
We can also learn how to apply ZKP to actual business by reading the circuit implementation of other projects, such as HarryR's ethsnarks-miximus (https://github.com/HarryR/ethsnarks-miximus) and Loopring's protocol3-circuits (https : //github.com/Loopring/protocol3-circuits). From these projects, you can learn how to engineeringly develop larger-scale circuits, and various design optimization details related to circuit performance. At the same time, you will have a deeper understanding of the scale of circuit constraints.
At the same time, everyone is welcome to continue to pay attention to the follow-up article of the "Zero-knowledge Proof: Learn by Coding: libsnark series" by Abe Labs. Next time we will try to combine zk-SNARKs and smart contracts, circuit modular development, more complex libsnark implementation cases, Further discussions are made on the pits that are easy to step on during circuit development.
7.Appendix
main.cpp
#include <libff / common / default_types / ec_pp.hpp>
#include <libsnark / common / default_types / r1cs_gg_ppzksnark_pp.hpp>
#include <libsnark / relations / constraint_satisfaction_problems / r1cs / examples / r1cs_examples.hpp>
#include <libsnark / zk_proof_systems / ppzksnark / r1cs_gg_ppzksnark / r1cs_gg_ppzksnark.hpp>
using namespace libsnark;
/ **
* The code below provides an example of all stages of running a R1CS GG-ppzkSNARK.
*
* Of course, in a real-life scenario, we would have three distinct entities,
* mangled into one in the demonstration below. The three entities are as follows.
* (1) The "generator", which runs the ppzkSNARK generator on input a given
* constraint system CS to create a proving and a verification key for CS.
* (2) The "prover", which runs the ppzkSNARK prover on input the proving key,
* a primary input for CS, and an auxiliary input for CS.
* (3) The "verifier", which runs the ppzkSNARK verifier on input the verification key,
* a primary input for CS, and a proof.
* /
template <typename ppT>
bool run_r1cs_gg_ppzksnark (const r1cs_example <libff :: Fr <ppT>> & example)
{
libff :: print_header ("R1CS GG-ppzkSNARK Generator");
r1cs_gg_ppzksnark_keypair <ppT> keypair = r1cs_gg_ppzksnark_generator <ppT> (example.constraint_system);
printf ("\ n"); libff :: print_indent (); libff :: print_mem ("after generator");
libff :: print_header ("Preprocess verification key");
r1cs_gg_ppzksnark_processed_verification_key <ppT> pvk = r1cs_gg_ppzksnark_verifier_process_vk <ppT> (keypair.vk);
libff :: print_header ("R1CS GG-ppzkSNARK Prover");
r1cs_gg_ppzksnark_proof <ppT> proof = r1cs_gg_ppzksnark_prover <ppT> (keypair.pk, example.primary_input, example.auxiliary_input);
printf ("\ n"); libff :: print_indent (); libff :: print_mem ("after prover");
libff :: print_header ("R1CS GG-ppzkSNARK Verifier");
const bool ans = r1cs_gg_ppzksnark_verifier_strong_IC <ppT> (keypair.vk, example.primary_input, proof);
printf ("\ n"); libff :: print_indent (); libff :: print_mem ("after verifier");
printf ("* The verification result is:% s \ n", (ans? "PASS": "FAIL"));
libff :: print_header ("R1CS GG-ppzkSNARK Online Verifier");
const bool ans2 = r1cs_gg_ppzksnark_online_verifier_strong_IC <ppT> (pvk, example.primary_input, proof);
assert (ans == ans2);
return ans;
}
template <typename ppT>
void test_r1cs_gg_ppzksnark (size_t num_constraints, size_t input_size)
{
r1cs_example <libff :: Fr <ppT>> example = generate_r1cs_example_with_binary_input <libff :: Fr <ppT>> (num_constraints, input_size);
const bool bit = run_r1cs_gg_ppzksnark <ppT> (example);
assert (bit);
}
int main () {
default_r1cs_gg_ppzksnark_pp :: init_public_params ();
test_r1cs_gg_ppzksnark <default_r1cs_gg_ppzksnark_pp> (1000, 100);
return 0;
}
#include <libsnark / common / default_types / r1cs_gg_ppzksnark_pp.hpp>
#include <libsnark / zk_proof_systems / ppzksnark / r1cs_gg_ppzksnark / r1cs_gg_ppzksnark.hpp>
#include <libsnark / gadgetlib1 / pb_variable.hpp>
using namespace libsnark;
using namespace std;
int main () {
typedef libff :: Fr <default_r1cs_gg_ppzksnark_pp> FieldT;
// Initialize the curve parameters
default_r1cs_gg_ppzksnark_pp :: init_public_params ();
// Create protoboard
protoboard <FieldT> pb;
// Define variables
pb_variable <FieldT> x;
pb_variable <FieldT> sym_1;
pb_variable <FieldT> y;
pb_variable <FieldT> sym_2;
pb_variable <FieldT> out;
// Allocate variables to protoboard
// The strings (like "x") are only for debugging purposes
out.allocate (pb, "out");
x.allocate (pb, "x");
sym_1.allocate (pb, "sym_1");
y.allocate (pb, "y");
sym_2.allocate (pb, "sym_2");
// This sets up the protoboard variables
// so that the first one (out) represents the public
// input and the rest is private input
pb.set_input_sizes (1);
// Add R1CS constraints to protoboard
// x * x = sym_1
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (x, x, sym_1));
// sym_1 * x = y
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (sym_1, x, y));
// y + x = sym_2
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (y + x, 1, sym_2));
// sym_2 + 5 = ~ out
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (sym_2 + 5, 1, out));
const r1cs_constraint_system <FieldT> constraint_system = pb.get_constraint_system ();
// generate keypair
const r1cs_gg_ppzksnark_keypair <default_r1cs_gg_ppzksnark_pp> keypair = r1cs_gg_ppzksnark_generator <default_r1cs_gg_ppzksnark_pp> (constraint_system);
// Add public input and witness values
pb.val (out) = 35;
pb.val (x) = 3;
pb.val (sym_1) = 9;
pb.val (y) = 27;
pb.val (sym_2) = 30;
// generate proof
const r1cs_gg_ppzksnark_proof <default_r1cs_gg_ppzksnark_pp> proof = r1cs_gg_ppzksnark_prover <default_r1cs_gg_ppzksnark_pp> (keypair.pk, pb.primary_input ()); pb.auxiliary_input ());
// verify
bool verified = r1cs_gg_ppzksnark_verifier_strong_IC <default_r1cs_gg_ppzksnark_pp> (keypair.vk, pb.primary_input (), proof);
cout << "Number of R1CS constraints:" << constraint_system.num_constraints () << endl;
cout << "Primary (public) input:" << pb.primary_input () << endl;
cout << "Auxiliary (private) input:" << pb.auxiliary_input () << endl;
cout << "Verification status:" << verified << endl;
}
#include <libsnark / common / default_types / r1cs_gg_ppzksnark_pp.hpp>
#include <libsnark / zk_proof_systems / ppzksnark / r1cs_gg_ppzksnark / r1cs_gg_ppzksnark.hpp>
#include <libsnark / gadgetlib1 / pb_variable.hpp>
#include <libsnark / gadgetlib1 / gadgets / basic_gadgets.hpp>
using namespace libsnark;
using namespace std;
int main () {
typedef libff :: Fr <default_r1cs_gg_ppzksnark_pp> FieldT;
// Initialize the curve parameters
default_r1cs_gg_ppzksnark_pp :: init_public_params ();
// Create protoboard
protoboard <FieldT> pb;
pb_variable <FieldT> x, max;
pb_variable <FieldT> less, less_or_eq;
x.allocate (pb, "x");
max.allocate (pb, "max");
pb.val (max) = 60;
comparison_gadget <FieldT> cmp (pb, 10, x, max, less, less_or_eq, "cmp");
cmp.generate_r1cs_constraints ();
pb.add_r1cs_constraint (r1cs_constraint <FieldT> (less, 1, FieldT :: one ()));
const r1cs_constraint_system <FieldT> constraint_system = pb.get_constraint_system ();
// generate keypair
const r1cs_gg_ppzksnark_keypair <default_r1cs_gg_ppzksnark_pp> keypair = r1cs_gg_ppzksnark_generator <default_r1cs_gg_ppzksnark_pp> (constraint_system);
// Add witness values
pb.val (x) = 18; // secret
cmp.generate_r1cs_witness ();
// generate proof
const r1cs_gg_ppzksnark_proof <default_r1cs_gg_ppzksnark_pp> proof = r1cs_gg_ppzksnark_prover <default_r1cs_gg_ppzksnark_pp> (keypair.pk, pb.primary_input ()); pb.auxiliary_input ());
// verify
bool verified = r1cs_gg_ppzksnark_verifier_strong_IC <default_r1cs_gg_ppzksnark_pp> (keypair.vk, pb.primary_input (), proof);
cout << "Number of R1CS constraints:" << constraint_system.num_constraints () << endl;
cout << "Primary (public) input:" << pb.primary_input () << endl;
cout << "Auxiliary (private) input:" << pb.auxiliary_input () << endl;
cout << "Verification status:" << verified << endl;
}
We will continue to update Blocking; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- QKL123 Blockchain List | Market Activity Declines, Mining Machines Are Renewed (201912)
- Alibaba releases 2019 annual report on counterfeiting: assisting 439 districts and counties to introduce counterfeiting
- Sending charcoal in the snow or contributing to the flames? See the pros and cons of CBDC from a practical perspective
- Review of the state of the blockchain network in 2019: BTC and ETH are far ahead
- Read a history of finance and regulation, as well as a lesson for DeFi
- Eight currencies that are about to halve in 2020
- "2019 Blockchain Panoramic Scan": Global Policy