Plasma-ish with ERC20, Part 1

Ko
Sotuu
Published in
9 min readMay 3, 2018

Blockchain is fascinating technology and we know Bitcoin is working pretty well as digital gold and Ethereum is working pretty well as platform for crowdfunding and a few applications like CryptKitty.

But not so good as digital currency because transaction speed is slow and transaction fee is not cheep enough for micro transaction.

That’s why many smart developers are working hard to solve so-called blockchain’s scaling problem and one of promising solution is called plasma.

The plasma is proposed by Vitalik Buterin and Joseph Poon, co founders of Ethereum.

As far as I understand the plasma is a technique to do computation off chain but still retain trust from the main net.

It’s kind of like magic.

If you use datastore like MySQL and MongoDB, it’s a centralized system. Even if you use private or consortium blockchain network, your chain is almost a centralized system because it’s a closed network. It’s almost just using blockchain as another security layer.

But what the plasma propose is even centralized system can borrow trust from the main net.

The Motivation

Here I want to show my plasma-ish implementation to use a private Ethereum network as child net.

I found 2 example implementations on Github but both examples seem to use centralized datastore like levelDB instead of blockchain and use UTXO.

I think child chain should be blockchain because it’s called child chain in the original plasma paper. And if you want grand child network, I think child net should be blockchain.

And since my intention is just make transactions where value transfer is not necessary, UTXO doesn’t fit well.

Plasma implementation examples

  1. PlasmaETHexchange from BankEx
  2. plasma-mvp from OmiseGo

If the plasma is not familiar to you, you can learn abstract here and other places.

Anyway I want to achieve…

  1. Make whatever transaction I want on private net
  2. Submit merkle root hash of transactions on private net to Ropsten test net
    * it will be the main net for the production
  3. Check proof of transaction on private net on parent net
  4. Transfer ERC20 from parent net to private (deposit)
  5. Transfer ERC20 from child net to parent (Exit)

I’m going to cover header submission and check proof part at part 1, and I will cover moving token between both chains part at part 2.

What I want to achieve

I want to setup a private ethereum blockchain as plasma child network. The point is I should be able to make any transactions on the child net and prove it on the main net.

To achieve this, I prepared 2 smart contracts and 1 Node script.

Smart contracts

  1. Plasma Child
    This smart contract exist in the private net and record which blockchain blocks belong to which plasma block and plasma header information
  2. Plasma Parent
    This contract exist in the main net and record plasma header information and used to check proof

Node script

Node script get transactions from Geth and generate the merkle root hash and submit plasma header information to both the main net and the private net.

Terminology

Parent net, the main net, and Ropsten test net is equivalent in this article. The main net and Ropsten test net work as parent network.

Child net and private net is equivalent in this article. Private net work as child network.

Submit Plasma Header

Now let’s create merkle root hash and submit it to the main net.

Prepare private key of Oracle Account

Because we need to sign a message manually, we need private key. Let’s retrieve a private key from public key ( Ethereum address) and password.

const keythereum = require("keythereum");const get_private_key = async () => {  var datadir = "<Path to Geth>";
var address= "<Ethereum address>";
var password = "<Password for the address>";
var keyObject = keythereum.importFromFile(address, datadir);
var privateKeyx = keythereum.recover(password, keyObject);
return privateKeyx.toString('hex')
}

Prepare smart contracts on the mainnet and private net

We need instances of smart contracts. Let’s initialize them here.

import Web3 from 'web3'// child networkconst childWeb3 = new Web3(new Web3.providers.HttpProvider("<URL of child net>"));
const PLASMA_CHILD_ADDRESS = "<address of child contract>"
import PLASMA_CHILD_JSON from './PlasmaChild_submitonly.json'
const plasma_child_instance = await new childWeb3.eth.Contract(PLASMA_CHILD_JSON.abi, PLASMA_CHILD_ADDRESS)// parent network
const parentWeb3 = new Web3(new Web3.providers.HttpProvider("<URL of main net>"));
const PLASMA_PARENT_ADDRESS = "<address of parent contract>"
import PLASMA_PARENT_JSON from './PlasmaParent_submitonly.json'
const plasma_parent_instance = await new parentWeb3.eth.Contract(PLASMA_PARENT_JSON.abi, PLASMA_PARENT_ADDRESS)

Lock contract

Let’s start submitting plasma header process!

const set_processing_true = async ({ instance }) => {  const estimateGas = await  instance.methods.setProcessing(true).estimateGas({
from: ORACLE_ADDRESS,
})
const results = await instance.methods.setProcessing(true).send({
from: ORACLE_ADDRESS,
gas: estimateGas,
})
}

Here I just set a parameter called processing as true.

Get current status

Next we need to get current parameters from child contract.
Because we include specific number of transactions in a plasma block.

  1. plasma block number
    The current serial number of plasma
  2. parent header information
    Information of last plasma header information. We need this to calculate parent hash which is used to verify data.
  3. max blockchain block number which is already included in plasma
    We start checking transactions from this number.
  4. latest blockchain block number of private net
    If we reached this number without finding enough transaction, we stop finding transactions here.

For example
Let’s say, we include 1,000 transactions in 1 plasma block and our current plasma block number is 7 and max blockchain block number is 8,109.

We keep checking our private net from blockchain block number 8110 until we find at least 1,000 transactions or we reach the latest block number of the private net.

# current plasma block number
const current_plasma_bn = await instance.methods.current_plasma_bn().call({
from: ORACLE_ADDRESS,
})
# parent plasma header informationparent_plasma_bn = current_plasma_bn - 1const plsma_header = await instance.methods.plasmaBlockHeaders(plasma_bn).call({
from: ORACLE_ADDRESS,
})
# max included blockchain block number of private net
const max_childnet_bn_included = await instance.methods.max_childnet_bn_included().call({
from: ORACLE_ADDRESS,
})
# get latest block_number
const blockNumber = await childWeb3.eth.getBlockNumber()

Generate merkle root hash

Finally we can generate merkle root hash

# https://github.com/BANKEX/PlasmaETHexchange/blob/master/lib/merkle-tools.js 
const MerkleTools = require('./merkle-tools');
const create_merkle_hash_and_txcount = async ({
max_childnet_bn_included,
target_count,
childnet_latest_block_number,
}) => {

// init merkleTree
const treeOptions = {
hashType: 'sha3'
}
const merkleTree = new MerkleTools(treeOptions)
// init transaction count
let tx_count_in_merkle = 0
// start block number
let idx = max_childnet_bn_included + 1
let loop_flg = true
while (loop_flg) {
let tx_count = await parentWeb3.eth.getBlockTransactionCount(idx)
// if we found transactions, add retrieve raw transaction and add it to merkle tree
if (tx_count !== 0) {

for (var j=0; j<tx_count; j++){
let tx = await parentWeb3.eth.getTransactionFromBlock(idx, j)
// create tx object
const tx2 = new EthereumTx(tx);
// get serialized transaction
const serializedTx = tx2.serialize();
// rawTx
const rawTx = "0x"+serializedTx.toString('hex')
// get sha256 of rawTx
const leaf = ethUtil.sha3(rawTx, 256).toString("hex")
// add leaf in merkle tree
merkleTree.addLeaf(leaf);
// increase the count
tx_count_in_merkle = tx_count_in_merkle + 1
}
}
if (childnet_latest_block_number === idx) {
// we reach the latest blockchain block number
loop_flg = false
} else if (tx_count_in_merkle > target_count) {
// we find enough transactions
loop_flg = false
} else {
// increment blockchain block number and find transactions again
idx = idx + 1
}
}
// create merkle tree
merkleTree.makeTree(false);
// get root hash
const merkleHashx = merkleTree.getMerkleRoot()
const rtn_object = {
merkleHashx: merkleHashx,
tx_count_in_merkle: tx_count_in_merkle,
max_bn_will_include: idx,
}
return rtn_object
}

Calculate parent hash

we need parent hash too to check data. If this is first plasma block, parent hash can be 0x00000000000000000000000000000000.

// create parent hash
const get_parentHash = ({ parent_plasma_header }) => {
const header = parent_plasma_header let parentHashx if ( parseInt(header.blockNumber) === 0) {
parentHashx = ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(0)), 32);
} else {
const arr = [
ethUtil.toBuffer("\x19Ethereum Signed Message:\n"),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(137)), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(header.blockNumber)), 4),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(header.numTransactions)), 4),
ethUtil.setLengthLeft(ethUtil.toBuffer(header.previousBlockHash), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(header.merkleRootHash), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(header.v)), 1),
ethUtil.setLengthLeft(ethUtil.toBuffer(header.r), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(header.s), 32),
]
const parentHash = childWeb3.utils.sha3(Buffer.concat(arr))
parentHashx = new Buffer(parentHash.substr(2), 'hex');
}
return parentHashx
}

Submit plasma header to the main net

To submit plasma header to the main net, we need to prepare hex data to submit. Then just submit the data to the parent contract.

// generate hex data to submit
const generate_data_to_submit_plasma_header = ({
current_plasma_bn,
tx_count_in_merkle,
merkleHashx,
parentHashx
}) => {
const arr = [
ethUtil.toBuffer("\x19Ethereum Signed Message:\n"),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(72)), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(current_plasma_bn)), 4),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(tx_count_in_merkle)), 4),
parentHashx,
merkleHashx,
]
const privateKeyx = ethUtil.toBuffer("0x"+ORACLE_KEY)
const block_sig = ethUtil.ecsign(msgHashx, privateKeyx)
// this is for next
let arr2 = [
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(current_plasma_bn)), 4),
ethUtil.setLengthLeft(ethUtil.toBuffer(new BN(tx_count_in_merkle)), 4),
parentHashx,
merkleHashx,
ethUtil.setLengthLeft(ethUtil.toBuffer(block_sig.v), 1),
ethUtil.setLengthLeft(ethUtil.toBuffer(block_sig.r), 32),
ethUtil.setLengthLeft(ethUtil.toBuffer(block_sig.s), 32)
]
const hexData = ethUtil.addHexPrefix(Buffer.concat(arr2).toString('hex'));
return {
hexData: hexData,
block_sig: block_sig
}
}
# submit data
const submit_plasma_header_to_parent = async ({ hexData, instance }) => {
const estimateGas = await instance.methods.submitBlock(hexData).estimateGas({
from: ORACLE_ADDRESS,
})
const results = await instance.methods.submitBlock(hexData).send({
from: ORACLE_ADDRESS,
gas: estimateGas,
})
return results
}

Update data on child contract

Save plasma header information and update current plasma block number and max included blockchain block number of private net.

const complete_plasma_child = async ({
instance,
current_plasma_bn,
tx_count_in_merkle,
merkleHashx,
block_sig,
max_bn_will_include,
parentHashx,
transactionHash,
}) => {
const merkleHash = "0x"+merkleHashx.toString("hex")
const parentHash = "0x"+parentHashx.toString("hex")
const sig_v = block_sig.v
const sig_r = "0x"+block_sig.r.toString("hex")
const sig_s = "0x"+block_sig.s.toString("hex")
const estimateGas = await instance.methods.completePlasmaBlockHeader(
current_plasma_bn,
tx_count_in_merkle,
parentHash,
merkleHash,
sig_v,
sig_r,
sig_s,
transactionHash,
max_bn_will_include,
).estimateGas({
from: ORACLE_ADDRESS,
})
const results = await instance.methods.completePlasmaBlockHeader(
current_plasma_bn,
tx_count_in_merkle,
parentHash,
merkleHash,
sig_v,
sig_r,
sig_s,
transactionHash,
max_bn_will_include,
).send({
from: ORACLE_ADDRESS,
gas: estimateGas,
})
return results
}

That’s it. You can check plasma header information both in the main net and private net.

Check Proof

All our effort is to borrow trust from the main net. To do that we need to verify a specific transaction on the main net. Let’s try to check a transaction.

// check a specific transaction on the main net
const test_proof = async () => {
try {
// init merkleTree
const treeOptions = {
hashType: 'sha3'
}
const merkleTree = new MerkleTools(treeOptions)
const plasma_bn = <plasma blocknumber>
const rawTx_to_prove = "<raw transaction>"
// prepare contract instance
const plasma_child_instance = await new childWeb3.eth.Contract(PLASMA_CHILD_JSON.abi, PLASMA_CHILD_ADDRESS)
const plasma_parent_instance = await new childWeb3.eth.Contract(PLASMA_PARENT_JSON.abi, PLASMA_PARENT_ADDRESS)
// get block from block number
const parent_plasma_header = await get_plasma_header({
instance: plasma_child_instance,
plasma_bn: plasma_bn,
})
// create merkle tree
const s_idx = parseInt(parent_plasma_header.start_childnet_bn)
const e_idx = parseInt(parent_plasma_header.end_childnet_bn) + 1;
let index_in_merkle_tree = 0
let idx_of_rawTx_in_merkle = -1
// search transaction until we find it.
for (var idx=s_idx; idx<e_idx; idx++){
const tx_count = await parentWeb3.eth.getBlockTransactionCount(idx)
if (tx_count !== 0) {
for (var j=0; j<tx_count; j++){
const tx = await parentWeb3.eth.getTransactionFromBlock(idx, j)
// create tx object
const tx2 = new EthereumTx(tx);
// get serialized transaction
const serializedTx = tx2.serialize();
// rawTx
const rawTx = "0x"+serializedTx.toString('hex')
if (rawTx_to_prove === rawTx) {
idx_of_rawTx_in_merkle = index_in_merkle_tree
}
// get sha256 of rawTx
const leaf = ethUtil.sha3(rawTx, 256).toString("hex")
merkleTree.addLeaf(leaf);
index_in_merkle_tree = index_in_merkle_tree + 1
}
}
}
// create merkle tree
merkleTree.makeTree(false);
// generate proof
const proof = ethUtil.bufferToHex(Buffer.concat(merkleTree.getProof(idx_of_rawTx_in_merkle, true)));
// just check
const results = await plasma_parent_instance.methods.checkProofOnly(
plasma_bn,
idx_of_rawTx_in_merkle,
rawTx_to_prove,
proof
).call({
from: ORACLE_ADDRESS,
})
} catch (err) {
console.log(err)
}
}

I create entire merkle tree again from the private net. This may not be so efficient and I should save all transactions in MongoDb or other data stores.

Anyway, you can prove a specific transaction happened in a specific plasma block on the main net.

But if I want real trust, just submitting merkle root hash may not be enough because user need raw transaction data, plasma block number the transaction is included, and index of the transaction in the merkle tree to generate proof.

If user can not retrieve such information form 3rd party, this mechanism is not so helpful. But if multiple party keep a private node and 3rd party can keep transactions and provide proof generating service, it’s a trustable way.

You can check entire scripts and smart contracts here. I think you can use these scripts as template to start plasma-ish mechanism.

I’m going to implement move ERC20 between the main net and private net at my future post.

We’re going to use this plasma-ish technique in our decentralized speech AI project called Sotuu.

You can check the code here.

Thanks for reading.

Acknowledgments

Most of difficult parts come from BankEx.
Thank you so much. I don’t think I could do this without you guys!

--

--

Ko
Sotuu
Editor for

I'm a serial entrepreneur. I enjoy AI, UI, and blockchain. I like history and reading too.