Search code examples
rubytypescompiler-errorsblockchaincrystal-lang

How to convert to Crystal ruby's multiple assignments of Array


I have a small (formerly) ruby blockchain script I'm trying to convert over into Crystal, that looks like this so far:

#  build your own blockchain from scratch in crystal!
#
#  to run use:
#    $ crystal ./blockchain_with_proof_of_work.cr

require "openssl" # for hash checksum digest function SHA256

class Block
  getter index : Int32
  getter timestamp : Time
  getter data : String
  getter previous_hash : String
  getter nonce : Int32 # # proof of work if hash starts with leading zeros (00)
  getter hash : String

  def initialize(index, data, previous_hash)
    @index = index
    @timestamp = Time.now
    @data = data
    @previous_hash = previous_hash
    @nonce, @hash = compute_hash_with_proof_of_work
  end

  def compute_hash_with_proof_of_work(difficulty = "00")
    nonce = 0
    loop do
      hash = calc_hash_with_nonce(nonce)
      if hash.starts_with?(difficulty)
        return [nonce, hash] # # bingo! proof of work if hash starts with leading zeros (00)
      else
        nonce += 1 # # keep trying (and trying and trying)
      end
    end
  end

  def calc_hash_with_nonce(nonce = 0)
    sha = OpenSSL::Digest.new("SHA256")
    sha.update(nonce.to_s + @index.to_s + @timestamp.to_s + @data + @previous_hash)
    sha.hexdigest
  end

  def self.first(data = "Genesis") # create genesis (big bang! first) block
    # # uses index zero (0) and arbitrary previous_hash ("0")
    Block.new(0, data, "0")
  end

  def self.next(previous, data = "Transaction Data...")
    Block.new(previous.index + 1, data, previous.hash)
  end
end # class Block

#####
# # let's get started
# #   build a blockchain a block at a time

b0 = Block.first("Genesis")
b1 = Block.next(b0, "Transaction Data...")
b2 = Block.next(b1, "Transaction Data......")
b3 = Block.next(b2, "More Transaction Data...")

blockchain = [b0, b1, b2, b3]

puts blockchain

######
#  will print something like:
#
# [#<Block:0x1e204f0
#   @data="Genesis",
#   @hash="00b8e77e27378f9aa0afbcea3a2882bb62f6663771dee053364beb1887e18bcf",
#   @index=0,
#   @nonce=242,
#   @previous_hash="0",
#   @timestamp=2017-09-20 20:13:38 +0200>,
#  #<Block:0x1e56e20
#   @data="Transaction Data...",
#   @hash="00aae8d2e9387e13c71b33f8cd205d336ac250d2828011f5970062912985a9af",
#   @index=1,
#   @nonce=46,
#   @previous_hash=
#    "00b8e77e27378f9aa0afbcea3a2882bb62f6663771dee053364beb1887e18bcf",
#   @timestamp=2017-09-20 20:13:38 +0200>,
#  #<Block:0x1e2bd58
#   @data="Transaction Data......",
#   @hash="00ea45e0f4683c3bec4364f349ee2b6816be0c9fd95cfd5ffcc6ed572c62f190",
#   @index=2,
#   @nonce=350,
#   @previous_hash=
#    "00aae8d2e9387e13c71b33f8cd205d336ac250d2828011f5970062912985a9af",
#   @timestamp=2017-09-20 20:13:38 +0200>,
#  #<Block:0x1fa8338
#   @data="More Transaction Data...",
#   @hash="00436f0fca677652963e904ce4c624606a255946b921132d5b1f70f7d86c4ab8",
#   @index=3,
#   @nonce=59,
#   @previous_hash=
#    "00ea45e0f4683c3bec4364f349ee2b6816be0c9fd95cfd5ffcc6ed572c62f190",
#   @timestamp=2017-09-20 20:13:38 +0200>]

However when I run it I get an error that states:

Error in blockchain.cr/blockchain_with_proof_of_work.cr:57: instantiating 
'Block:Class#first(String)'

b0 = Block.first("Genesis")
           ^~~~~

in blockchain.cr/blockchain_with_proof_of_work.cr:45: instantiating 
'Block:Class#new(Int32, String, String)'

Block.new(0, data, "0")
      ^~~

in blockchain.cr/blockchain_with_proof_of_work.cr:22: instance variable 
'@nonce' of Block must be Int32, not (Int32 | String)

    @nonce, @hash = compute_hash_with_proof_of_work
    ^~~~~~

Looking at Crystal docs on multiple assignment, I'm unsure of how I can refactor this method so that it doesn't fail Crystal's automatic static type checking and type inference? The method in question, of an array of two types being returned, doesn't seem covered by the docs:

@nonce, @hash = compute_hash_with_proof_of_work # return [nonce, hash]

Solution

  • When decomposing an Array into a multiple assignment Crystal can't infer the exact type of each element. So the value assigned to the instance variable @nonce could be either Int32 or String. You should use a Tuple instead: return {nonce, hash} (in line 29). A tuple has positional type declarations and is by the way more performant than an Array because it does not allocate memory on the heap.