2
votes

I'm a programmer with a C++ and Python background who recently stumbled upon Julia, and I really like what it has to offer. In order to become more familiar with both blockchain implementation and Julia at the same time, I'm being a little ambitious and am trying to create a basic implementation of a blockchain in Julia by converting the Python implementation posted by Hackernoon (The author explains what each method is supposed to do better than I ever could).

However, I'm running into issues when creating the actual Blockchain struct. In order to create the genesis block, Hackernoon suggests I call the member function new_block in the constructor. So far, I haven't been able to figure out how to best replicate this in Julia. Here's what I have so far:

import JSON
import SHA

mutable struct Blockchain
    chain::Array{Dict{String, Any}}
    current_transactions::Array{String}
    block::Dict{String, Any}
    new_block::Function

    function Blockchain(chain::Array{Dict{String, Any}}, current::Array{String})    
        new(chain, current, new_block(previous_hash=1, proof=100)) 
        # ^issue calling function within the constructor here
    end
end

When I try to run my code, I get the following error:

invalid redefinition of constant Blockchain.

Here's the new_block function:

function new_block(proof::String, previous_hash::String=nothing)
    block = Dict(
    "index" => length(chain) + 1,
    "timestamp" => time(),
    "transactions" => current_transactions,
    "proof" => proof,
    "previous_hash" => previous_hash | hash(chain[end]),
    )
    current_transactions = []
    append!(chain, block)
    return block
end

Here are the rest of the functions I currently have:

function new_transaction(this::Blockchain, sender::String, recipient::String, amount::Int)
    
     transaction = Dict(
        "sender"=> sender,
        "recipient"=> recipient,
        "amount"=> amount,
     )

    append!(this.current_transactions, transaction)

    return length(this.chain) + 1
end

function hash(block::Blockchain)
    block_string = JSON.dumps(block, sort_keys=true).encode()
    return sha256(block_string).hexdigest()
end

I might have some misconceptions about how types/structs work in Julia; most of my information was obtained from third party websites along with the official documentation. Here are some of the sources I've been relying on:

Smarter/more efficient ways of trying to accomplish what I am would be immensely appreciated.

Edit:

Here are some of the changes I've made, based on given suggestions:

struct Blockchain
    chain::Array{Dict{String, Any}}
    current_transactions::Array{String}

    function Blockchain(chain::Array{Dict{String, Any}}, current::Array{String})    
        new(chain, current)
    end
end

function new_block!(this::Blockchain, proof::Int, previous_hash::Int=nothing)
    block = Dict(
    "index" => length(this.chain) + 1,
    "timestamp" => time(),
    "transactions" => this.current_transactions,
    "proof" => proof,
    )
    if previous_hash == nothing
        block["previous_hash"] = hash(this.chain[end])
    else
        block["previous_hash"] = previous_hash
    end

    this.current_transactions = []
    append!(this.chain, block)
    return this
end

I realized that the block attribute was useless, as it only existed to be added to the chain, so I removed it.

Additionally, here is an alternate Blockchain definition without the inner constructor:

struct Blockchain
    chain::Array{Dict{String, Any}}
    current_transactions::Array{String}
    
    Blockchain(x::Array{Dict{String, Any}}, y::Array{String}) = new(x, y)
end
1
Hi Victor. First the error message invalid redefinition of constant Blockchain isn't related to your new_block function stuff at all. It's just that you can't redefine a type (struct) in Julia. Once it's defined you can't change it anymore.carstenbauer
The error that you really get when calling your constructor Blockchain(chain, current) is ERROR: UndefVarError: new_block not defined which makes perfect sense. I'd suggest to not put the new_block function into the struct at all. Typically in Julia you put "attributes" into the struct and functions separately.carstenbauer
So your new_block should simply take a Blockchain object as first argument. You can than access and modify fields of this Blockchain.carstenbauer
That makes sense. Does that mean that the constructor should go outside of the struct along with new_block?victor-alves
@victor-alves, constructor can stay within the struct definition. however, inner constructors will have another effect on your code, i.e., no default constructor will be generated by Julia's compiler. Basically, your original problem has nothing to do with your constructor, nor new_block. You cannot redefine the type, as the error already suggests. Then, when you solve that problem, you will face another one because you are trying to call part of an object which is to be created (not yet done)Arda Aytekin

1 Answers

2
votes

DISCLAIMER. This may not necessarily be an answer to your question. But I wanted to post it as an answer, as a comment does not allow me to express the below that easily.

mutable struct Blockchain
    chain::Array{Dict{String, Any}}
    current_transactions::Array{String}
    block::Dict{String, Any}
    new_block::Function

    function Blockchain(chain::Array{Dict{String, Any}}, current::Array{String})    
        new(chain, current, new_block(previous_hash=1, proof=100)) 
        # ^issue calling function within the constructor here
    end
end

Here, I assume that you are trying to add some member function functionality to your struct, as you have already stated that you are coming from a C++ background. However, this is not Julian. In Julia, as @crstnbr has already suggested, we need to define global functions that act on objects. The convention is that you add ! at the end of the function to indicate that the function will be changing at least one of its arguments.

Then, by checking the definition of your new_block:

function new_block(proof::String, previous_hash::String=nothing)
    block = Dict(
    "index" => length(chain) + 1, # which chain?
    "timestamp" => time(),
    "transactions" => current_transactions, # which current_transactions?
    "proof" => proof,
    "previous_hash" => previous_hash | hash(chain[end]), # which chain?
    )
    current_transactions = []
    append!(chain, block) # which chain?
    return block
end

I notice a couple of serious mistakes. First, there are some undefined variables that you try to use such as chain and current_transactions. I assume, again from C++, you have thought that new_block would be a member function of Blockchain, and hence, could see its chain member variable. This is not how Julia works. Second problem is how you try to call new_block:

new_block(previous_hash=1, proof=100)

This call is totally wrong. The above call notation relies on keyword arguments; however, your function definition only has positional arguments. To be able to support keyword arguments, you need to change your function definition to read as below:

function new_block(; proof::String, previous_hash::String=nothing)
  #                ^ note the semi-colon here
  # ...
end

And last, you define proof and previous_hash to be of type String, but call them with 1, 100 and nothing, which are of type Int, Int and Void.

I could not understand your design choice for the Blockchain application in your mind, but I strongly suggest that you should go step-by-step with simpler examples to learn the language. For instance, if you just try the below examples, you will understand how type annotations work in Julia:

Main> f(s::String = nothing) = s
f (generic function with 2 methods)

Main> f()
ERROR: MethodError: no method matching f(::Void)
Closest candidates are:
  f(::String) at none:1
  f() at none:1
Stacktrace:
 [1] f() at ./none:1
 [2] eval(::Module, ::Any) at ./boot.jl:235

Main> g(s::String) = s
g (generic function with 1 method)

Main> g(100)
ERROR: MethodError: no method matching g(::Int64)
Closest candidates are:
  g(::String) at none:1
Stacktrace:
 [1] eval(::Module, ::Any) at ./boot.jl:235

Main> h1(var1 = 1, var2 = 100) = var1 + var2
h1 (generic function with 3 methods)

Main> h1(var2 = 5, var1 = 6)
ERROR: function h1 does not accept keyword arguments
Stacktrace:
 [1] kwfunc(::Any) at ./boot.jl:237
 [2] eval(::Module, ::Any) at ./boot.jl:235

One last comment is that as far as I can see from your example, you do not need mutable struct. struct should simply help you with your design --- you can still add to/modify its chain, current_transactions and block variables. Check, again, the simpler example below:

Main> struct MyType
         a::Vector{Float64}
       end

Main> m = MyType([1,2,3]);

Main> append!(m.a, 4);

Main> m
MyType([1.0, 2.0, 3.0, 4.0])

You can think of MyType's a variable, in the above example, as being double * const a in C++ terms. You are not allowed to change a to point to a different memory location, but you can modify the memory location pointed-to by a.

In short, you should definitely try to learn the language from the official documentation step-by-step, and post questions here involving really minimal examples. Your example is convoluted in that sense.