328 lines
16 KiB
Markdown
328 lines
16 KiB
Markdown
|
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](/assets/images/nim-crystal-header-img_NEW.jpg)
|
|||
|
|
|||
|
Welcome back to my series comparing the two sweethearts of the modern low-level programming world. Just to quickly recap: in [article #1](/news/2019/11/18/nim-vs-crystal-part-1-performance-interoperability/) 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](/news/2019/11/21/nim-vs-crystal-part-2-threading-tooling/); 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:
|
|||
|
|
|||
|
``` 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:
|
|||
|
|
|||
|
``` nim
|
|||
|
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](https://facebook.github.io/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:
|
|||
|
|
|||
|
- [Ionic Framework](http://ionicframework.com/)
|
|||
|
- [Flutter](https://flutter.io/)
|
|||
|
- [NativeScript](https://www.nativescript.org/)
|
|||
|
|
|||
|
And if you come from a Windows background:
|
|||
|
|
|||
|
- [Xamarin](https://dotnet.microsoft.com/apps/xamarin)
|
|||
|
|
|||
|
|
|||
|
### 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:
|
|||
|
|
|||
|
``` 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:
|
|||
|
|
|||
|
``` crystal
|
|||
|
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:
|
|||
|
|
|||
|
``` json
|
|||
|
{
|
|||
|
"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:
|
|||
|
|
|||
|
``` nim
|
|||
|
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](https://github.com/status-im/nim-eth/) contains a whole bunch of useful Nim libraries for interacting with the Ethereum Network, including:
|
|||
|
|
|||
|
- [Recursive Length Prefix encoding (RLP)](https://github.com/status-im/nim-eth/blob/master/doc/rlp.md),
|
|||
|
- [P2P](https://github.com/status-im/nim-eth/blob/master/doc/p2p.md),
|
|||
|
- [Eth-keys](https://github.com/status-im/nim-eth/blob/master/doc/keys.md),
|
|||
|
- [Eth-keyfile](https://github.com/status-im/nim-eth/blob/master/doc/keyfile.md),
|
|||
|
- [Ethereum Trie structure](https://github.com/status-im/nim-eth/blob/master/doc/trie.md), and
|
|||
|
- [Ethereum Bloom Filter](https://github.com/status-im/nim-eth/blob/master/doc/bloom.md).
|
|||
|
|
|||
|
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](https://nimbus.team) 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**](https://twitter.com/rbin)
|