Introducing ebakusDB (part 1)

ebakus
ebakus
Published in
8 min readSep 24, 2019

In this article we are going to build a Leaderboard dApp on ebakus using Solidity and ebakusDB. To make it more digestable we have split it in 2 parts. This first part of the tutorial will showcase how to leverage ebakusDB within your smart contract allowing you to handle large datasets with ease and in part 2 we will see in detail how to access the data you stored there from any website or application.

A little about ebakusDB

ebakusDB is a transactional database we have built from scratch that we use as a core element of the ebakus node. As you can see in this benchmark results table, its really fast and in part is what enables the ebakus node to scale that well. It supports multiple indexes and schema defined tables and after serving us that well in the node we decided to also expose it to the EVM and make it available to Solidity for smart contract development. It really changes what is possible to do with Solidity. It makes it so much easier to handle large datasets and implement projects like exchanges and complex games. But enough on the sales pitch lets see how we can use it.

What we are building

We are building a smart contract capable of storing multiple leaderboards. Each leaderboard can store multiple player scores and have ascending or descending ordering. This leaderboard smart contract can be integrated or set up to accept calls from another smart contract that handles game logic for a game dApp. Alternatively, it could also be a part of a game that has its game logic in a non-decentralized context and just uses ebakus to store its leaderboard system.

The data structure

To implement the leaderboard system we need to create the following data structure:

Since Solidity in Ethereum does not provide a database this would have to be stored in state storage as Solidity mapping to structs, which would in turn result in high costs in terms of gas and several limitations as the dataset grows. Moreover performing queries on any struct field would be impossible. Ebakus with ebakusDB provides a game changer solution to these problems.

The Basics: Declaring, Creating and inserting rows

All you have to do is to get the latest version of EbakusDB.sol from our repository, import it to your solidity code and declare tables as you can see in the following example.

Let’s start by declaring our Leaderboard table:

pragma solidity ^0.5.6;

import "./EbakusDB.sol";
contract GameBoard {
string LeaderboardsTable = "Leaderboards";

struct Leaderboard {
uint64 Id;
string Title;
string Image;
uint8 Order; // 0: ASC, 1: DESC
}

constructor() public {
string memory leaderboardsTableAbi = '[{"type":"table","name":"Leaderboards","inputs":[{"name":"Id","type":"uint64"},{"name":"Title","type":"string"},{"name":"Image","type":"string"},{"name":"Order","type":"uint8"}]}]';
EbakusDB.createTable(LeaderboardsTable, "Title", leaderboardsTableAbi);

}
}

More specifically after importing EbakusDB.sol, we have to:

  1. Declare a struct with the corresponding variables to our model as described above
  2. Define the ABI for this struct as seen at leaderboardsTableAbi
  3. Create the actual table
EbakusDB.createTable(String tableName, String indexes, String Abi);

(if there are multiple indexes, it can be a comma seperated string of the indexes)

Now that we have our table we can create the function that inserts leaderboards. To do this we will use the insertObj method of ebakusDB defined bellow:

EbakusDB.insertObj(String tableName, bytes AbiEncodedData)

To insert the row into the table.

function insertLeaderboard(uint64 _id, string calldata _title, string calldata _image, uint8 _order) external onlyOwner {
require(bytes(_title).length < 255);
require(bytes(_image).length <= 400);
require(_order <= 1);

Leaderboard memory l = Leaderboard(_id, _title, _image, _order);
bytes memory input = abi.encode(l.Id, l.Title, l.Image, l.Order);

bool ok = EbakusDB.insertObj(LeaderboardsTable, input);
require(ok);

emit NewLeaderboard(l.Id, l.Title, l.Image, l.Order);
}

As we see its important to manually Abi encode your struct data before calling the insertObj function.

So far we have created the leaderboards table and have the function to insert new leaderboards to it.

Yay! You have already learned the basics to use ebakusDB, its that simple! You can now query it through web3 and read the data from the client.

Now that we‘ve seen the basics let’s do some practice. We will make our smart contract more useful by adding some more functionality that allows us to set player scores to a leaderboard entry. Lets start by creating the Scores table as following:

event ScoreSet(uint64 leaderboardId, address userId, uint64 value);struct Score {
string Id; // userId + leaderboardId
uint64 LeaderboardId;
address UserId;
uint64 Value;
}
EbakusDB.createTable(...)

and create the function to insert scores.

function setScore(uint64 _leaderboardId, address _userId, uint64 _value) external onlyOwner {
string memory id = string(abi.encodePacked(_userId, _leaderboardId));

Score memory s = Score(id, _leaderboardId, _userId, _value);
bytes memory input = abi.encode(s.Id, s.LeaderboardId, s.UserId, s.Value);

bool ok = EbakusDB.insertObj(ScoresTable, input);
require(ok);

emit ScoreSet(s.LeaderboardId, s.UserId, s.Value);
}

Query the ebakusDB within the smart contract

This would work for all intents and purposes but we can improve further with some sanity checks. Let’s do the basics and check if that leaderboard we are trying to insert the score into exists before accepting the score. If we want to execute this logic within the smart contract we need to create a getter function for the leaderboard table called getLeaderboard(id) like bellow

function getLeaderboard(uint64 _id) public view returns (uint64, string memory, string memory, uint8) {
Leaderboard memory l;
bytes memory out = EbakusDB.get(LeaderboardsTable, string(abi.encodePacked("Id = ", uint2str(_id))), "");
(l.Id, l.Title, l.Image, l.Order) = abi.decode(out, (uint64, string, string, uint8));
return (l.Id, l.Title, l.Image, l.Order);
}
// helper solidity function
function uint2str(uint _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while (_i != 0) {
bstr[k--] = byte(uint8(48 + _i % 10));
_i /= 10;
}
return string(bstr);
}

As you can see to implement this we also had to create the uint2str helper function that as the name suggests turns unsigned integers to strings.

Now that we have the logic to query the Leaderboards table for entries with a specific id we can further improve our setScore function as bellow.

function setScore(uint64 _leaderboardId, address _userId, uint64 _value) external onlyOwner {
// get leaderboard from DB and verify
Leaderboard memory l;
(l.Id, l.Title, l.Image, l.Order) = getLeaderboard(_leaderboardId);
require(l.Id == _leaderboardId);

string memory id = string(abi.encodePacked(_userId, _leaderboardId));

Score memory s = Score(id, _leaderboardId, _userId, _value);
bytes memory input = abi.encode(s.Id, s.LeaderboardId, s.UserId, s.Value);

bool ok = EbakusDB.insertObj(ScoresTable, input);
require(ok);

emit ScoreSet(s.LeaderboardId, s.UserId, s.Value);
}

Putting everything together…

To put everything together the final contract should look like this.

pragma solidity ^0.5.6;

import "./EbakusDB.sol";

contract GameBoard {
event NewLeaderboard(uint64 id, string title, string image, uint8 order);
event ScoreSet(uint64 leaderboardId, address userId, uint64 value);

string LeaderboardsTable = "Leaderboards";
string ScoresTable = "Scores";

struct Leaderboard {
uint64 Id;
string Title;
string Image;
uint8 Order; // 0: ASC, 1: DESC
}


struct Score {
string Id; // userId + leaderboardId
uint64 LeaderboardId;
address UserId;
uint64 Value;
}


address private owner;

modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function.");
_;
}

constructor() public {
owner = msg.sender;

string memory leaderboardsTableAbi = '[{"type":"table","name":"Leaderboards","inputs":[{"name":"Id","type":"uint64"},{"name":"Title","type":"string"},{"name":"Image","type":"string"},{"name":"Order","type":"uint8"}]}]';

EbakusDB.createTable(LeaderboardsTable, "Title", leaderboardsTableAbi);

string memory scoresTableAbi = '[{"type":"table","name":"Scores","inputs":[{"name":"Id","type":"string"},{"name":"LeaderboardId","type":"uint64"},{"name":"UserId","type":"address"},{"name":"Value","type":"uint64"}]}]';

EbakusDB.createTable(ScoresTable, "Value", scoresTableAbi);
}

function createLeaderboard(uint64 _id, string calldata _title, string calldata _image, uint8 _order) external onlyOwner {
require(bytes(_title).length < 255);
require(bytes(_image).length <= 400);
require(_order <= 1);

Leaderboard memory l = Leaderboard(_id, _title, _image, _order);
bytes memory input = abi.encode(l.Id, l.Title, l.Image, l.Order);

bool ok = EbakusDB.insertObj(LeaderboardsTable, input);
require(ok);

emit NewLeaderboard(l.Id, l.Title, l.Image, l.Order);
}

function setScore(uint64 _leaderboardId, address _userId, uint64 _value) external onlyOwner {
// get leaderboard from DB and verify
Leaderboard memory l;
(l.Id, l.Title, l.Image, l.Order) = getLeaderboard(_leaderboardId);
require(l.Id == _leaderboardId);

string memory id = string(abi.encodePacked(_userId, _leaderboardId));

Score memory s = Score(id, _leaderboardId, _userId, _value);
bytes memory input = abi.encode(s.Id, s.LeaderboardId, s.UserId, s.Value);

bool ok = EbakusDB.insertObj(ScoresTable, input);
require(ok);

emit ScoreSet(s.LeaderboardId, s.UserId, s.Value);
}




function getLeaderboard(uint64 _id) public view returns (uint64, string memory, string memory, uint8) {
Leaderboard memory l;
bytes memory out = EbakusDB.get(LeaderboardsTable, string(abi.encodePacked("Id = ", uint2str(_id))), "");
(l.Id, l.Title, l.Image, l.Order) = abi.decode(out, (uint64, string, string, uint8));
return (l.Id, l.Title, l.Image, l.Order);
}

function uint2str(uint _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len - 1;
while (_i != 0) {
bstr[k--] = byte(uint8(48 + _i % 10));
_i /= 10;
}
return string(bstr);
}
}

Our leaderboard contract is now complete! We can now use https://remix.ebakus.com to deploy it to the ebakus testnet.

Since this tutorial is about ebakusDB we won’t get into the specifics of using remix. The only thing that changes in the ebakus version of ethereum’s remix is that instead of using metamask or something similar it uses the embedded ebakus wallet library.

If you want to skip deploying the contract above you can use the one we deployed here:

0x01e8CFCda40D475dE6b25498683e8A9566276491

To be able to write as contract owner, you can use the following mnemonic:

delay arch fault nut table smoke thing confirm exhaust online swap gym

Using ebakus-web3 to interface with the ebakus network and ebakusDB

After deploying your contract on the ebakus network you can use web3-ebakus library allows reads from ebakusDB. The web3-ebakus library essentially extends ethereum’s web3 with ebakus specific functions such us

  • web3.eth.suggestDifficulty(address) that queries the node for the suggested target difficulty needed for the PoW in order for the transaction to enter a block, taking into account current congestion levels and address stake.
  • web3.eth.calculateWorkForTransaction(txObject, suggestedDifficultyValue) that returns the transaction along the hash that derived through proof of work needed to for the tx to enter a block. We call it before sending each tx and we pass its result as an argument to web3.eth.sendTransaction(txWithPow)
  • web3.db.select(contractAddress, tableName, whereCondition, orderByColumn, blockNumber) allows for performing selects with conditions ordered by column name. More specifically:
    - contractAddress: contract address that created the DB tables
    -
    tableName: table name
    -
    whereClause: where condition for finding an entry
    — Supported conditions are “<”, “>”, “=”, “==”, “<=”, “>=”, “!=”, “LIKE”. Example use case: ‘Phone = “555–1111”’
    -
    orderClause: order clause for sorting the results using “ASC” or “DESC”
    — Example use case: ‘Phone DESC’
    -
    blockNumber: block number to read from EbakusDB state. You can use latest string for fetching from latest block.
  • web3.db.next(iter) returns the next result of the select performed through web3.db.select(…)
  • web3.db.get(contractAddress, tableName, whereCondition, orderByColumn, blockNumber), it returns a single entry and works just like web3.db.select

you can read more about how we use web3-ebakus to interact with the ebakus network on our article: Migrating your dApp from ethereum to ebakus but we will also release a part 2 of this article where we will present in detail how you can fetch leaderboards, create new ones and set scores to specific ones.

If you are feeling adventurous check out the indy-leaderboard repository in our github to see how we would suggest to implement a leaderboards/achievements system with best practices in mind. Both on a smart contract but also on the client level.

Reach us at telegram and twitter, we’d love to hear from you.

--

--

ebakus
ebakus
Editor for

Novel blockchain solution that enables dApps to reach mass adoption. 1 sec blocks DPOS, with free transactions, ebakusDB and backwards compatible with Ethereum