Intro to Gun - A P2P Decentralized Database
With all the attention and hype that web3 technology gets, it’s easy to forget that it’s not the only (or even the best) way to build all decentralized software. The core benefit that we get from blockchains is an immutable, historical ledger of every transaction that has ever occurred on the chain. The killer feature is the historical record.
Let’s compare something like Bitcoin and Lightning. Bitcoin is a blockchain with a token, while Lightning is a P2P layer on top of Bitcoin. Why doesn’t Lightning have a token? Because Lightning is ephemeral.
Someone can open a Lightning channel, perform their operations, and then when Lightning writes the final data back to the Bitcoin change, the Lightning channel is erased along with the transactions that occurred on it.
P2P technology has existed for a while, but it differs in that the data stored on a P2P system is ephemeral, there is no guarantee in the system that the history of everything will be available throughout time.
This historical ledger is the foundational reason why Bitcoin was transformational and why blockchains have tokens. Maintaining this historical record is expensive, and tokens are meant to serve as an incentive for individual node operators to maintain this historical ledger.
Since writing and maintaining data in a blockchain system is expensive and slow, it’s not ideal for storing large amounts of data among lots of people.
One other difference between a blockchain like Bitcoin and a P2P system like what we’ll be looking at today is that all nodes on the Bitcoin have to store all the data, this is a key feature of a blockchain network.
But in a P2P network, nodes can choose what data they host on their node. This makes them more lightweight and flexible but not as good for needing to store a complete history of shared data across the entire network.
What to Store on the Blockchain
But this brings us to an interesting problem. What if we want to build software that is completely decentralized? In order for the ideal vision of web3 to become a reality, we need to have things become more and more decentralized over time, not just centralized facades over a couple of smart contracts.
But because operating smart contracts is expensive, it’s not feasible or ideal to have all of our data live and be operated on in them. We should only put what is absolutely necessary in smart contracts, data for which we need to have a permanent historical record.
Things like financial transactions, such as moving fungible or non-fungible tokens around, are good examples of what to store on-chain. But a good chunk of other data can still be stored in a decentralized way but is not suited for a blockchain. We want to keep that data to a minimum.
It’s a common pitfall of newer web3 developers to think that in order for our data to be trustless and decentralized it has to be stored in a smart contract, but this isn’t true.
Let’s say we are creating a decentralized course platform where creators can make courses and accept cryptocurrency as payment from students. Should we store all of the data for a dapp like this on-chain? Definitely not.
A more pragmatic approach would be to let smart contracts handle the actual purchasing of courses, potential NFTs, and any DAO functionality the platform might have, but what about things like user profile data and course content?
This would be problematic to store on-chain for two reasons:
- It would be very expensive to store this large amount of data on-chain
- It would be a massive undertaking to change the data structure of our dapp, we would have to come up with a complex contract upgrade process any time we need to add a basic database field
A common criticism of web3 skeptics is that the majority of a dapp stack is just as centralized as anything else and that if nothing but the smart contracts is decentralized, rendering the dapp unusable in the case of a compromised frontend or database, then it’s pointless.
There are two possible ways to handle this scenario.
One is to use a traditional centralized database solution in conjunction with our smart contracts, and simply store a hash of our data in our smart contract, and update that when a database change is made. This way we can verify the integrity of our database utilizing our smart contract.
If the hashes don’t match, the data is compromised.
Another way is to use a P2P decentralized database solution like Gun.
Fundamentals of Gun
Gun is a decentralized P2P database. In most traditional database solutions, even if the data is distributed across multiple nodes, those nodes are all ultimately controlled by a single entity.
This means that when you use apps on 99% of the Internet, they own any data you add there. With a P2P model like Gun, each individual node owns its own data and shares it as needed.
In a P2P system like Gun, nodes can choose what data they share and aren’t required to share anything.
Gun is a real-time, offline-first, decentralized, P2P graph database.
Gun allows for seamless data synchronization between nodes and its offline-first features make it so that if one of the nodes loses connectivity, the app can store changes locally and update when the connection is restored.
This makes Gun an excellent choice for building decentralized software that needs real-time capability.
We’ll be building an MVP for a hypothetical social media app called Remington that will allow us to post messages to a feed visible to everyone.
In order to follow along best, you’ll probably want to be familiar with React and hooks, although if you have programming experience you will probably be able to follow along.
You can also view the repo of the code we’ll be writing.
Project Setup
We’ll be using create-react-app
in order to create this app, I’m a fan of Yarn so let’s get started by running yarn create react-app remington
.
If that worked we should be able to switch into that directory and run yarn start
to view our new app at localhost:3000
.
Now let’s install gun with yarn add gun --save
.
To keep things simple, we’ll be doing everything inside the App.js
file.
First we need to import Gun up at the top along with useEffect
and useState
, which we’ll be utilizing here soon.
import Gun from 'gun'
import { useState, useEffect } from 'react'
The first thing we need to do is instantiate our Gun instance.
We can get that set up within our App
function component.
let gun = Gun();
Alright now we have the ability to connect to a Gun relay peer, which will attempt to store all data. Unlike a blockchain network like Bitcoin or Ethereum, which forces all nodes to store all the data, Gun nodes (or peers) can store whatever data they want.
Usually this is dictated by what data they are subscribed to, but Gun also has these relay peers which will attempt to store all data in the network, making things work better.
But what if the peer I am connected to is not storing the data I need?
This is where Gun gets really cool. It uses DAM, or Daisy-chain Ad-hoc Mesh-network, as its P2P networking algorithm.
DAM is very technical, but basically, it allows peers to communicate with each other to get the data that they need. If you are interested, you can dive into the technical details.
Okay so now that we have established our connection to a peer, let’s see if we can add some data.
Gun is a P2P database, which means everyone shares and can access the same data. So we can add anything we want to the top level of the graph database and it will be available for anyone to retrieve.
We won’t be setting up any user functionality or authentication in this tutorial, but that would be an excellent exercise for you to dive into in order to understand Gun and how it handles security at a deeper level.
For now, we’re going to add our data at the top level but add a reference to it in a set called posts
. This will tell any nodes that they only need to subscribe to data from this particular set in order to sync our application’s data.
The code to do this is very simple.
First we’ll create a new state object called posts
which will serve as our reference to this set.
const [posts, setPosts] = useState();
Next we’ll create another useEffect
hook to set this data.
useEffect(() => {
setPosts(gun.get("posts"));
}, []);
Here’s what our App.js
file should look like now.
import { useEffect, useState } from "react";
import Gun from "gun";
import logo from "./logo.svg";
import "./App.css";
function App() {
const [posts, setPosts] = useState();
let gun = Gun()
useEffect(() => {
setPosts(gun.get("posts"));
}, []);
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
Now let’s look at actually adding something to the Gun DB. Gun allows you to structure data pretty much however you want, so we are free to choose our own data structure here.
When we create a new item, Gun will automatically generate and assign a soul
to the item, which serves as its unique ID in the shared Gun database.
Let’s set up a very simple form with some state to add a new post. In order to do that, make your App.js
file look like this, then we’ll add the Gun-specific stuff together.
import { useEffect, useState } from "react";
import Gun from "gun";
import logo from "./logo.svg";
import "./App.css";
function App() {
const [posts, setPosts] = useState();
const [postContent, setPostContent] = useState("");
let gun = Gun();
useEffect(() => {
setPosts(gun.get("posts"));
}, []);
const handlePostContentChange = (event) => {
event.persist();
setPostContent(event.target.value);
};
const handleFormSubmit = (e) => {
e.preventDefault();
// Add post data to Gun
};
return (
<div className="App">
<header className="App-header">
<form onSubmit={handleFormSubmit}>
<input
type="text"
name="postContent"
value={postContent}
onChange={handlePostContentChange}
/>
<button type="submit">Add Post</button>
</form>
</header>
</div>
);
}
export default App;
Alright now we have a super basic form.
Where that comment is in the form submission handler is where we’ll actually add our data to Gun. Replace that comment with this.
posts.set(postContent);
setPostList([...postList, postContent]);
setPostContent("");
We need a couple of other pieces of code for this to work. We need to add our new state variables and another useEffect
call to update our list of posts.
First the new state variables.
const [postList, setPostList] = useState([]);
Next the new useEffect
call.
useEffect(() => {
if (posts !== undefined) {
const postArray = [];
posts.map().on((post, id) => {
postArray.push(post);
});
setPostList(postArray);
}
}, [posts]);
Our file should now look like this.
import { useEffect, useState } from "react";
import Gun from "gun";
import "./App.css";
function App() {
const [posts, setPosts] = useState();
const [postList, setPostList] = useState([]);
const [postContent, setPostContent] = useState("");
let gun = Gun();
useEffect(() => {
setPosts(gun.get("posts"));
}, []);
useEffect(() => {
if (posts !== undefined) {
const postArray = [];
posts.map().on((post, id) => {
postArray.push(post);
});
setPostList(postArray);
}
}, [posts]);
const handlePostContentChange = (event) => {
event.persist();
setPostContent(event.target.value);
};
const handleFormSubmit = (e) => {
e.preventDefault();
posts.set(postContent);
setPostList([...postList, postContent]);
setPostContent("");
};
return (
<div className="App">
<header className="App-header">
<form onSubmit={handleFormSubmit}>
<input
type="text"
name="postContent"
value={postContent}
onChange={handlePostContentChange}
/>
<button type="submit">Add Post</button>
</form>
{postList !== undefined
? postList.map((post, index) => {
return <p key={index}>{post}</p>;
})
: ""}
</header>
</div>
);
}
export default App;
If we run our app, we should be able to add items to the post
set we’ve created within Gun and see them added to the list in real time.
What we’ve done here is told Gun that the items we want to add and view are coming from the posts
set, which can be thought of as a table.
When we submit the form, we are adding to that set.
When we load the page to get the list, we are iterating through that set in order to pull the items contained within it.
Wrap Up
In this tutorial we looked a bit at Gun, the decentralized P2P database. As we begin to discover and build tools that move us closer towards the ethos we’ve all set up around decentralized software, we’ll start to discover and invent things that will bring us closer to that ideal.
There’s a lot more to explore with Gun, and you can create fully-features, fully-functional database using it.
Gun is one of the tools that has the potential to bring us closer to a decentralized, user-owned Internet, if we can just cultivate the desire to use it and other tools like it. Using tools like Gun in conjunction with blockchain-based tools depending on the specific use case is how we’ll build functional applications that are actually useful.