Fork me on GitHub
Smooth, easy blockchain apps. Powered by Tendermint consensus.

Introduction

Lotion is a new way to create blockchain apps in JavaScript, which aims to make writing new blockchains fast and fun. It builds on top of Tendermint using the ABCI protocol. Lotion lets you write secure, scalable applications that can easily interoperate with other blockchains on the Cosmos Network using IBC.

Lotion itself is a tiny framework; its true power comes from the network of small, focused modules built upon it. Adding a fully-featured cryptocurrency to your blockchain, for example, takes only a few lines of code.

Note: the security of this code has not yet been evaluated. If you expect your app to secure real value, please use Cosmos SDK instead.

Installation

Lotion requires node v7.6.0 or higher.

$ npm i lotion
$ node my-lotion-app.js

Application Server

A lotion application is usually a single function of signature (state, tx) which mutates your blockchain's state in response to a transaction. All of your application state is contained within a single JavaScript object.

As a developer, all you need to do is design your application's initial state, then write the function to compute the next state when a transaction happens.

Here's a minimal hello world example which simply counts the number of transactions that have occurred so far:

let app = require('lotion')({
  initialState: { count: 0 }
})

app.use((state, tx) => {
  state.count++
})

app.listen(3000)

This will start the lotion application http server on port 3000, which you can use to query the state of the blockchain and create new transactions.

Blockchains and Tendermint

The goal of a blockchain is to represent a single state being concurrently edited. In order to avoid conflicts between concurrent edits, it represents the state as a ledger: a series of transformations applied to an initial state. The blockchain must allow all connected nodes to agree about which transformations are valid, and their ordering within the ledger.

To accomplish this, a blockchain is composed of three protocols: the network protocol, consensus protocol, and transaction protocol.

The network protocol is how nodes in the network tell each other about new transactions, blocks, and other nodes; usually a p2p gossip network.

The consensus protocol is the set of rules that nodes should follow to determine which particular ordered set of transformations should be in the ledger at a given moment. In Bitcoin, the chain with the highest difficulty seen by a node is treated as authoritatively correct.

The transaction protocol describes what makes transactions valid, and how they should mutate the blockchain's state.

When you're writing a lotion app, you're only responsible for writing the transaction protocol. Under the hood, Tendermint is handling the consensus and network protocols. When you start your lotion app, a Tendermint node is also started which will handle all of the communication with other nodes running your lotion app.

Transaction Handlers

Transaction handlers are the heart and soul of your application. Just mutate the state according to the data contained in the transaction.

Adding some middleware to handle a transaction is easy:

let lotion = require('lotion')
let app = lotion(opts)

function txHandler(state, tx){
  if(tx.someNumber > state.highestNumberEverSeen) {
    state.highestNumberEverSeen = tx.someNumber
  }
}

app.use(txHandler)
app.listen(3000)

There are some crucial things to be aware of when you're writing this function, however:

Middleware chaining

You can set up multiple transaction handlers on your app like so:

let app = require('lotion')(opts)

app.use(firstTxHandler)
app.use(secondTxHandler)
   .use(thirdTxHandler)
   .listen(3000)

Chain info

Sometimes the way you'll handle a transaction will depend on info about the chain, such as the current block height, rather than just application state. In those situations, your transaction handler function can accept a third parameter to access information about the chain itself.

chainInfo.height

As a contrived example to illustrate block height usage, here's how you'd write an app that only accepts transactions after 100 empty blocks have passed:

function txHandler(state, tx, chainInfo) {
  if(chainInfo.height > 100){
    state.lastTxHeight = chainInfo.height
  }
}

chainInfo.validators

Validators are the nodes that get to propose new blocks and vote on which ones are valid. Sometimes you need to change their relative voting power. Simply change their voting power on chainInfo.validators.

Here's an example app that increases the voting power of each validator by 1 after each transaction (the unit doesn't matter -- it's relative voting power):

function txHandler(state, tx, chainInfo) {
  for(let pubKey in chainInfo.validators) {
    // pubKey is hex-encoded validator public key.
    chainInfo.validators[pubKey]++
  }
}

Block Handlers

You probably only need to write transaction handlers. But sometimes it's helpful to apply some logic to your blockchain's state once per block. For example, maybe you'd like to implement some demurrage schedule.

You can pass a block handler to your app with app.useBlock when configuring your lotion server. It accepts (state, chainInfo) and will be called exactly once per block to optionally mutate your state, even if there haven't been any transactions since the last block.

Here's a modified version of the counter app that adds 1 to a count for each transaction, but subtracts 1 from the count for each block:

let lotion = require('lotion')
let app = lotion({ initialState: { count: 0 }})

function txHandler(state, tx, chainInfo) {
  state.count++
}

function blockHandler(state, chainInfo) {
  state.count--
}

app.use(txHandler)
app.useBlock(blockHandler)

app.listen(3000)

As a guideline, putting logic in transaction handlers is preferred instead of writing a block handler where possible.

HTTP API

The lotion application server will be listening on port 3000 (or whatever port you specified instead) to let you query the state of the blockchain and create new transactions.

GET /state

This will return your app's most recent state as JSON.

$ curl http://localhost:3000/state

# {"count": 0}

POST /txs

Create a new transaction and submit it to the network.

$ curl http://localhost:3000/txs -d '{}'

# {"state": {"count": 1},"ok": true}

Ecosystem

Lotion is a small library, but its API is explicitly designed to spawn an ecosystem of interoperable applications, tools, and modules.

Here's a curated list of some software in the lotion ecosystem:

Modules

Applications

Dev tools

Making a cryptocurrency

Let's make a new coin on lotion.

Here's the code we're going to end up with:

let secp256k1 = require('secp256k1')
let { randomBytes } = require('crypto')
let createHash = require('sha.js')
let vstruct = require('varstruct')
let axios = require('axios')

let TxStruct = vstruct([
  { name: 'amount', type: vstruct.UInt64BE },
  { name: 'senderPubKey', type: vstruct.Buffer(33) },
  { name: 'senderAddress', type: vstruct.Buffer(32) },
  { name: 'receiverAddress', type: vstruct.Buffer(32) },
  { name: 'nonce', type: vstruct.UInt32BE }
])

exports.handler = function(state, rawTx) {
  let tx = deserializeTx(rawTx)
  if (!verifyTx(tx)) {
    return
  }

  let senderAddress = tx.senderAddress.toString('hex')
  let receiverAddress = tx.receiverAddress.toString('hex')

  let senderBalance = state.balances[senderAddress] || 0
  let receiverBalance = state.balances[receiverAddress] || 0

  if(senderAddress === receiverAddress) {
    return
  }
  if (!Number.isInteger(tx.amount)) {
    return
  }
  if (tx.amount > senderBalance) {
    return
  }
  if (tx.nonce !== (state.nonces[senderAddress] || 0)) {
    return
  }
  senderBalance -= tx.amount
  receiverBalance += tx.amount

  state.balances[senderAddress] = senderBalance
  state.balances[receiverAddress] = receiverBalance
  state.nonces[senderAddress] = (state.nonces[senderAddress] || 0) + 1
}

function hashTx(tx) {
  let txBytes = TxStruct.encode({
    amount: tx.amount,
    senderPubKey: tx.senderPubKey,
    senderAddress: tx.senderAddress,
    nonce: tx.nonce,
    receiverAddress: tx.receiverAddress
  })
  let txHash = createHash('sha256')
    .update(txBytes)
    .digest()

  return txHash
}

function signTx(privKey, tx) {
  let txHash = hashTx(tx)
  let signedTx = Object.assign({}, tx)
  let { signature } = secp256k1.sign(txHash, privKey)
  signedTx.signature = signature

  return signedTx
}

function verifyTx(tx) {
  if (
    deriveAddress(tx.senderPubKey).toString('hex') !==
    tx.senderAddress.toString('hex')
  ) {
    return false
  }
  let txHash = hashTx(tx)
  return secp256k1.verify(txHash, tx.signature, tx.senderPubKey)
}

function serializeTx(tx) {
  let serialized = Object.assign({}, tx)
  for (let key in tx) {
    if (Buffer.isBuffer(tx[key])) {
      serialized[key] = tx[key].toString('base64')
    }
  }
  return serialized
}

function deserializeTx(tx) {
  let deserialized = tx
  ;[
    'senderPubKey',
    'senderAddress',
    'receiverAddress',
    'signature'
  ].forEach(key => {
    deserialized[key] = Buffer.from(deserialized[key], 'base64')
  })

  return deserialized
}

function deriveAddress(pubKey) {
  return createHash('sha256')
    .update(pubKey)
    .digest()
}

exports.client = function(url = 'http://localhost:3232') {
  let methods = {
    generatePrivateKey: () => {
      let privKey
      do {
        privKey = randomBytes(32)
      } while (!secp256k1.privateKeyVerify(privKey))

      return privKey
    },
    generatePublicKey: privKey => {
      return secp256k1.publicKeyCreate(privKey)
    },
    generateAddress: pubKey => {
      return deriveAddress(pubKey)
    },
    getBalance: async (address) => {
      let state = await axios.get(url + '/state').then(res => res.data)
      return state.balances[address] || 0
    },
    send: async (privKey, { address, amount }) => {
      let senderPubKey = methods.generatePublicKey(privKey)
      let senderAddress = methods.generateAddress(senderPubKey)

      let currentState = await axios.get(url + '/state').then(res => res.data)

      let nonce = currentState.nonces[senderAddress.toString('hex')] || 0

      let receiverAddress
      if (typeof address === 'string') {
        receiverAddress = Buffer.from(address, 'hex')
      } else {
        receiverAddress = address
      }
      let tx = {
        amount,
        senderPubKey,
        senderAddress,
        receiverAddress,
        nonce
      }

      let signedTx = signTx(privKey, tx)
      let serializedTx = serializeTx(signedTx)
      let result = await axios.post(url + '/txs', serializedTx)
      return result.data
    }
  }

  return methods
}