Skip to content

Instantly share code, notes, and snippets.

@NicMcPhee
Created November 3, 2015 23:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NicMcPhee/838ea3659eebd98cdb66 to your computer and use it in GitHub Desktop.
Save NicMcPhee/838ea3659eebd98cdb66 to your computer and use it in GitHub Desktop.
Distributed cryptographically fair coin toss in Elixir with multiple nodes.
# Person A flips and Person B chooses "heads" or "tails".
# Person B wins if their choice matches the flip, otherwise
# person A wins.
defmodule CoinToss do
def run_simulation(first_host, second_host, num_people) do
counterPid = spawn(__MODULE__, :counter_loop, [0, 0])
:global.register_name(:counter, counterPid)
people = create_people(first_host, second_host, num_people)
pair_all(people)
end
def create_people(first_host, second_host, num_people) do
Enum.map 0..num_people-1, &(make_person(first_host, second_host, &1))
end
def make_person(first_host, second_host, name) do
host = case :crypto.rand_uniform(0, 2) do
0 -> first_host
1 -> second_host
end
{ Node.spawn(host, __MODULE__, :wait, [name]), name }
end
def pair({firstPID, firstName}, {secondPID, secondName}) do
send(firstPID, {:start, secondPID, secondName})
end
def pair_all([]) do true end
def pair_all([p, q | others]) do
pair(p, q)
pair_all(others)
end
def update_counter(choice, result) do
counterPid = :global.whereis_name(:counter)
send(counterPid, {choice, result})
end
def getReport() do
counterPid = :global.whereis_name(:counter)
send(counterPid, {:report})
end
def counter_loop(heads_wins, tails_wins) do
receive do
{:report} ->
IO.puts("#{heads_wins} heads wins and #{tails_wins} tails wins.\n")
{:heads, :won} ->
IO.puts("Called heads and won")
counter_loop(heads_wins+1, tails_wins)
{:tails, :lost} ->
IO.puts("Called tails and lost")
counter_loop(heads_wins+1, tails_wins)
{:heads, :lost} ->
IO.puts("Called heads and lost")
counter_loop(heads_wins, tails_wins+1)
{:tails, :won} ->
IO.puts("Called tails and won")
counter_loop(heads_wins, tails_wins+1)
end
end
def wait(myName) do
# We should be using the newer :rand module from Erlang, but it's not in
# the version in the lab.
# We need to see separately for each player since (apparently) every
# player's random number generator is in its own process space in Elixir.
:random.seed(:os.timestamp)
receive do
{:start, partnerID, partnerName} ->
IO.puts("Player #{myName} is flipping for Player #{partnerName}.")
# We've been asked to flip
# This would be nicer with the Elixir Enum.random([:heads, :tails])
# but that's new and not in the versions we have in the lab.
flip = case :random.uniform(2)-1 do
0 -> :tails
1 -> :heads
end
IO.puts("The flip for Player #{myName} was #{flip}.")
{salt, hashed_flip} = hash_flip(flip)
send(partnerID, {:choose, self(), myName, hashed_flip})
wait_for_choice(partnerID, partnerName, myName, flip, salt, hashed_flip)
{:choose, partnerID, partnerName, hashed_flip} ->
IO.puts("Player #{myName} is choosing for Player #{partnerName}.")
# The fib call is just to put some expensive computation in
# the system so we can see the parallelism on the system monitor.
# Feel free to remove this, or raise or lower the argument to change
# the amount of work being done here.
_ = fib(30+:random.uniform(10))
# We've been asked to choose heads or tails
# This would be nicer with the Elixir Enum.random([:heads, :tails])
# but that's new and not in the versions we have in the lab.
choice = case :random.uniform(2)-1 do
0 -> :tails
1 -> :heads
end
IO.puts("The choice for Player #{myName} was #{choice}.")
send(partnerID, {:check_outcome, self(), myName, choice, hashed_flip})
wait_for_outcome(partnerID, partnerName, myName, choice, hashed_flip)
end
end
# This actor has flipped, and is waiting to receive their
# partner's choice.
def wait_for_choice(partnerID, partnerName, myName, flip, salt, hashed_flip) do
IO.puts("Player #{myName} is waiting for outcome from Player #{partnerName}.")
receive do
{:check_outcome, ^partnerID, ^partnerName, choice, ^hashed_flip} ->
IO.puts("Player #{myName} recieved :check_outcome from #{partnerName}.")
# The result here is from the perspective of the chooser
# not the flipper.
result = case flip == choice do
true -> :won
false -> :lost
end
IO.puts("Player #{myName} checked: Flip was #{flip}, choice was #{choice}, result was #{result}")
send(partnerID, {:confirm_result, myName, result, flip, salt, hashed_flip})
end
end
# This actor has chosen, and waiting to receive and confirm the result.
def wait_for_outcome(partnerID, partnerName, myName, choice, hashed_flip) do
IO.puts("Player #{myName} is waiting for the result from Player #{partnerName}.")
receive do
{:confirm_result, ^partnerName, result, flip, salt, ^hashed_flip} ->
IO.puts("Player #{myName} received :confirm_result message from #{partnerName}.")
case (choice == flip) == (result == :won) do
true -> true
false -> exit("Player #{partnerName} didn't process toss correctly")
end
{^salt, hash_check} = hash_flip(salt, flip)
case hash_check do
hashed_flip -> true;
_ -> exit("Player #{partnerName}'s hashed flip didn't match")
end
IO.puts("Processing choice #{choice} and result #{result}")
update_counter(choice, result)
end
end
defp hash_flip(flip) do
salt = to_string(:crypto.rand_uniform(0, 1000000))
hash_flip(salt, flip)
end
defp hash_flip(salt, flip) do
{salt, :crypto.md5(salt <> to_string(flip))}
end
defp fib(0) do 0 end
defp fib(1) do 1 end
defp fib(n) do fib(n-1) + fib(n-2) end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment