embark/site/source/_posts/2019-11-28-nim-vs-crystal-p...

16 KiB
Raw Blame History

title: Nim vs Crystal - Part 3 - Crypto, DApps & P2P summary: "Crystal and Nim go head-to-head to figure out the best modern, low-level programming language! In part 3; Crypto, P2P & DApps are explored." author: robin_percy categories:

  • tutorials layout: blog-post image: '/assets/images/nim-crystal-header_blank.jpg'

crystal vs nim

Welcome back to my series comparing the two sweethearts of the modern low-level programming world. Just to quickly recap: in article #1 I noted my thoughts on the interoperability capabilities of the two languages, alongside briefly reviewing the performance metrics for each (albeit with relatively simple tests). Whether simple or not, the tests did throw up some unexpected twists in the plot. Crystal used very-nearly half of the memory amount executing the tests when compared to Nim, and also took very nearly half of the execution time in doing so. This seriously took me by surprise!

In article #2; I looked at the Concurrency primitives of each language, and explored both the in-built tooling, and external package ecosystems surrounding each language. As I said in that article, one of the biggest factors I look at when considering adopting a new language; is its tooling ecosystem. This includes, but is not limited to: A comprehensive package manager, an intuitive testing suite, a good project scaffolder, and an in-built formatter/linter to ensure my code stays semantically correct especially if I know I will be working in Open Source repos that others will contribute to. But they're just the high-level tools that I look for...

From a low-level standpoint; I look for efficient use of technology in features such as in-memory storage, caching, garbage collection, and concurrency primitives that not just markedly improve our application performance, but that are also relatively simple, and intuitive to use. I see this as particularly important as I have, in my past, seen some truly shocking examples of trying to handle multi-threading, from languages that I love *cough* Ruby *cough*. I also like to see a fully-featured standard library that takes influence from previous successful languages. However, I digress...

I regret to say that this is the final article in this series! It's been good fun for me; getting to the know the ins-and-outs of Nim, and to re-grow a fresh appreciation of Crystal, having put it on the back-burner for quite some time. However, whether the final article in the series or not, it's going to be a good one! We're going to be covering the benefits to the Cryptocurrency / DApp industries from both Crystal and Nim. So without further ado:

Let's dive on in!

Cryptocurrency

Firstly, I'd like to talk about the possibility of using either Crystal or Nim, (or both!) in the development of crypto apps. Hypothetically; if we had the inclination to build out our own Cryptocurrency: Crystal and Nim have proven to be two of the strongest languages to consider for the undertaking.. (That being the next blog series I'm going to write in the near future, so deciding which language to use will be heavily influenced by this blog series!)

For our Cryptocurrency, we would need to be able to use an intelligent key manager, utilise smart hashing algorithms, maintain strong performance, and all of this atop of a distributed, decentralised virtual machine or blockchain. Now, all of this sounds like a very tall order! For all of these feature requirements to be met by a single programming language, it would mean that this language is going to have to be ONE HELL of an impressive piece of technology.

Happily, both Crystal and Nim allow us all of the above functionality. In our hypothetical usecase, if we were to build out a fully-featured blockchain; mining and hashing functions would need to be continually made, both of which entail relatively heavy computations. As shown over the last 2 articles in the series, we can at least be sure that both langs can handle the performance stresses, no problemo.

As I'd like to write this topic out into a further detailed article series, I will show off just 2 of the above pieces of functionality we'd require for our Crypto app:

Calculating our Block Hashes

When building our Blockchain; we need to consider how we're going to identify and chain our transaction blocks together (blockchain). Without going into details in this article on how blockchains function, we'll stick with the existing, and proven, SHA256 algorithm.

In Crystal:

require "json"
require "openssl"

module OurCryptoApp::Model
  struct Transaction
    include JSON::Serializable

    alias TxnHash = String

    property from : String
    property to : String
    property amount : Float32
    getter hash : TxnHash
    getter timestamp : Int64

    def initialize(@from, @to, @amount)
      @timestamp = Time.utc_now.to_unix
      @hash = calc_hash
    end

    private def calc_hash : TxnHash
      sha = OpenSSL::Digest.new("SHA256")
      sha.update("#{@from}#{@to}#{@amount}#{@timestamp}")
      sha.hexdigest
    end
  end
end

In Nim:

If we want to generate a similar hash in Nim, we could run the following:

import strutils

const SHA256Len = 32

proc SHA256(d: cstring, n: culong, md: cstring = nil): cstring {.cdecl, dynlib: "libssl.so", importc.}

proc SHA256(s: string): string =
  result = ""
  let s = SHA256(s.cstring, s.len.culong)
  for i in 0 .. < SHA256Len:
    result.add s[i].BiggestInt.toHex(2).toLower

echo SHA256("Hash this block, yo")

Releasing our Crypto App

Another serious factor we have to consider, is the ability to distribute our crypto app, once built, with great ease. Remembering that both Crystal and Nim are compiled languages, we're already off to a promising start. (A single executable binary is always going to be easier to distribute than something requiring its own specialist environment!)

It pays rather large dividends being able to write our Crypto app just once, and having the ability to maintain one singular code repo for that app. To this end I think it is definitely worth considering a multi-platform app framework. I already know that in my next article series, I will be exploring building a Crypto app using React Native.

However, if you wish to build the frontend of your cross-platform crypto app in something else, there are a variety of technologies available - all of which seem to work well with both Crystal and Nim:

And if you come from a Windows background:

Building & Releasing In Nim:

If we wanted to build out and release our app for Android, we can run:

nim c -c --cpu:arm --os:android -d:androidNDK --noMain:on

To generate the C source files we need to include in our Android Studio project. We then simply add the generated C files to our CMake build script in our Android project.

Similarly, we could run:

nim c -c --os:ios --noMain:on

To generate C files to include in our XCode project. Then, we can use XCode to compile, link, package and sign everything.

Building & Releasing In Crystal:

Crystal also allows for cross-compilation, and makes it just as easy. For example, to build our app for Linux distributions from our Mac, we can run:

crystal build your_program.cr --cross-compile --target "x86_64-unknown-linux-gnu"

Worth noting: Crystal doesn't offer the out-of-the-box iPhone / Android cross-compilation functionality that Nim does, so building our app in Nim gets a definite thumbs-up from a distribution point-of-view!

Ethereum - Building, Signing & Sending a Transaction

For the sake of this article, in Crystal, I didn't see the need to write out a more low-level example of the below action, as it is so similar to the Nim demo that follows. This actually worked out in my favour, as it means I get to further show off the native HTTP library for Crystal.

In Crystal:

require "http/client"

module Ethereum
  class Transaction

    # /ethereum/create/ Create - Ethereum::Transaction.create(args)
    def self.create(to : String, from : String, amount : UInt64, gas_price : UInt64? = nil, gas_limit : UInt64? = nil) : EthereumToSign | ErrorMessage

      headers = HTTP::Headers.new
      if ENV["ONCHAIN_API_KEY"]? != nil
        headers.add("X-API-KEY", ENV["ONCHAIN_API_KEY"])
      end

      response = HTTP::Client.post "https://onchain.io/api/ethereum/create//?to=#{to}&from=#{from}&amount=#{amount}&gas_price=#{gas_price}&gas_limit=#{gas_limit}", headers: headers

      return ErrorMessage.from_json response.body if response.status_code != 200

      ethereumtosign = EthereumToSign.from_json response.body


      return ethereumtosign
    end

    # /ethereum/sign_and_send/ Sign and send - Ethereum::Transaction.sign_and_send(args)
    def self.sign_and_send(to : String, from : String, amount : UInt64, r : String, s : String, v : String, gas_price : UInt64? = nil, gas_limit : UInt64? = nil) : SendStatus | ErrorMessage

      headers = HTTP::Headers.new
      if ENV["ONCHAIN_API_KEY"]? != nil
        headers.add("X-API-KEY", ENV["ONCHAIN_API_KEY"])
      end

      response = HTTP::Client.post "https://onchain.io/api/ethereum/sign_and_send//?to=#{to}&from=#{from}&amount=#{amount}&r=#{r}&s=#{s}&v=#{v}&gas_price=#{gas_price}&gas_limit=#{gas_limit}", headers: headers

      return ErrorMessage.from_json response.body if response.status_code != 200

      sendstatus = SendStatus.from_json response.body


      return sendstatus
    end

  end
end

Then, in our application we could simply call:

Ethereum::Transaction.create("0xA02378cA1c24767eCD776aAFeC02158a30dc01ac", "0xA02378cA1c24767eCD776aAFeC02158a30dc01ac", 80000)

And we would get a response similar to the following, ready to be signed and sent to the Ethereum network:

{
  "tx": "02000000011cd5d7621e2a7c9403e54e089cb0b5430b83ed13f1b897d3e319b100ba1b059b01000000db00483045022100d7534c80bc0a42addc3d955f74e31610aa78bf15d79ec4df4c36dc98e802f5200220369cab1bccb2dbca0921444ce3fafb15129fa0494d041998be104df39b8895ec01483045022100fe48c4c1d46e163acaff6b0d2e702812d20",
  "hash_to_sign": "955f74e31610aa78bf15d79ec4df4c36dc98e802f52002"
}

In Nim:

From a deeper, more low-level perspective; instead of using an HTTP library as in the Crystal example above, we can use Status' very own Nim-Ethereum library to build our Ethereum transaction. Assuming we have imported nim-eth into our Nimble project, our Ethereum transaction can be built atop of the following protocol:

import
  nim-eth/[common, rlp, keys], nimcrypto

proc initTransaction*(nonce: AccountNonce, gasPrice, gasLimit: GasInt, to: EthAddress,
  value: UInt256, payload: Blob, V: byte, R, S: UInt256, isContractCreation = false): Transaction =
  result.accountNonce = nonce
  result.gasPrice = gasPrice
  result.gasLimit = gasLimit
  result.to = to
  result.value = value
  result.payload = payload
  result.V = V
  result.R = R
  result.S = S
  result.isContractCreation = isContractCreation

type
  TransHashObj = object
    accountNonce:  AccountNonce
    gasPrice:      GasInt
    gasLimit:      GasInt
    to {.rlpCustomSerialization.}: EthAddress
    value:         UInt256
    payload:       Blob
    mIsContractCreation {.rlpIgnore.}: bool

proc read(rlp: var Rlp, t: var TransHashObj, _: type EthAddress): EthAddress {.inline.} =
  if rlp.blobLen != 0:
    result = rlp.read(EthAddress)
  else:
    t.mIsContractCreation = true

proc append(rlpWriter: var RlpWriter, t: TransHashObj, a: EthAddress) {.inline.} =
  if t.mIsContractCreation:
    rlpWriter.append("")
  else:
    rlpWriter.append(a)

const
  EIP155_CHAIN_ID_OFFSET* = 35

func rlpEncode*(transaction: Transaction): auto =
  # Encode transaction without signature
  return rlp.encode(TransHashObj(
    accountNonce: transaction.accountNonce,
    gasPrice: transaction.gasPrice,
    gasLimit: transaction.gasLimit,
    to: transaction.to,
    value: transaction.value,
    payload: transaction.payload,
    mIsContractCreation: transaction.isContractCreation
    ))

func rlpEncodeEIP155*(tx: Transaction): auto =
  let V = (tx.V.int - EIP155_CHAIN_ID_OFFSET) div 2
  # Encode transaction without signature
  return rlp.encode(Transaction(
    accountNonce: tx.accountNonce,
    gasPrice: tx.gasPrice,
    gasLimit: tx.gasLimit,
    to: tx.to,
    value: tx.value,
    payload: tx.payload,
    isContractCreation: tx.isContractCreation,
    V: V.byte,
    R: 0.u256,
    S: 0.u256
    ))

func txHashNoSignature*(tx: Transaction): Hash256 =
  # Hash transaction without signature
  return keccak256.digest(if tx.V.int >= EIP155_CHAIN_ID_OFFSET: tx.rlpEncodeEIP155 else: tx.rlpEncode)

Note - I do realise the above Nim code example and the Crystal examples are different - I fully intended them to be. The Crystal example allowed me to further show off the HTTP library I touched on in the last article, and the Nim example allowed me to go to a lower-level; something I think brings the article relevancy full circle.

Status' Eth Common Library contains a whole bunch of useful Nim libraries for interacting with the Ethereum Network, including:

If you are going to be working in the Ethereum ecosystem using Nim, it goes without saying that these utilities are absolutely essential. With Status & the Nimbus team being such early adopters and major contributors to the Nim/Crypto universe, you are more than likely to stumble across this code sooner or later!

Conclusion

Our hypothetical Crypto app has taken shape throughout this article, and I think both languages have shown off great promise, and have proven their respective abilities to power the Cryptocurrency universe.

Realistically, if you were a brand-new developer looking to learn a language to break into the Crypto scene, the choice would almost definitely be Crystal. This is simply because of the much larger ecosystem and resources surrounding it.

However, if you were an already-established developer, looking to build out a crypto app that you could develop and multi-platform release with greater ease, you'd inevitably choose Nim. Crystal not only lacks the ability to be developed properly on Windows, but also lacks the interoperability and multi-release functionality, as we have seen, with Nim.

Alas, this brings me on to my final points...

Series Conclusion

It's funny each article in this series, I've started by saying to myself "Right, Nim is going to win." And then half way through; changing my story to "Crystal is my choice, actually."

But then I went and spoiled it all, by saying something stupid like "Cryptocurrency".

Prior to this article, I was swaying towards settling on Crystal. Not only did it impress in performance, but also seemed to have an enthusiastic ecosystem building around it. Nim, however, refused to go down without a fight offering up extremely impressive interoperability, awesome inbuilt tooling, and great efficiency overall.

I hate to do this, but I'm just going to have to say it: for your usecase pick the best tool for the job. Please ensure that you research properly into both languages, and weigh-up the pro's/con's that pertain to your specific usecase.

Cliches aside if I had to pick a favourite overall language, it would have to be Crystal. Frankly, this opinion is formed from my extensive use of Crystal over Nim, the fact I much prefer the Crystal syntax, and the fact that I am simply more comfortable coding in Crystal than I am in Nim!

So, to answer the epic question Crystal vs Nim?

Personally, I choose Crystal. But I think you should choose Nim. 😅

- @rbin