Flipping a Coin Using Hashing
Overview
Imagine we have two people: Alice and Bob. They want to bet on the outcome of a coin flip but do not trust each other to reveal the true outcome of the event. The provided solution is an example of a commitment scheme where one can commit to a chosen value while keeping it hidden to others, with the ability to reveal the committed value later.
Problem Solution
We can solve this problem using a cryptographic hash function. Alice can commit to heads or tails by sharing the hash of the result. Since there are only two results, it would be trivial for Bob to work out whether it was a heads or tails from the hash. Therefore Alice hashes the outcome of the event with a secret nonce so that it is impossibly difficult to reverse the hash.
To be exact, we use the following scheme:
- Alice flips a coin and hashes the outcome with a nonce
- Alice sends the hashed value to Bob and keeps the result and nonce secret
- Bob sends his guess of the outcome to Alice
- Alice reveals the result of the coin flip to Bob and reveals the nonce used
Alice is unable to lie about the outcome due to the nature of hashes; it’s infeasible to find two values that hash to the same result. Bob would be able to verify that the hash of the result and nonce in step 4
, does not match what Alice originally sent in step 2
.
To facilitate the betting, we can store the funds in an escrow/contract account and once the result is known, the winner can claim all the money.
Link Stateful and Stateless Smart Contracts
As stated, we will make use of an escrow account to store the bets and pay out the winner. To know which account won the bet, we need to link the escrow account to a stateful smart contract that facilitates the coin flip.
We will link the two by following these steps in order:
- Deploy the application and gets its’ application ID
- Hardcode the application ID into the escrow stateless smart contract and compile it to get the escrow address
- Store the escrow address in the application global state
If we are in the same directory as the PyTeal files, we can first compile the PyTeal stateful smart contract into TEAL using:
python3 stateful.py > stateful.teal
python3 clear.py > clear.teal
and deploy the application with:
goal app create --creator <CREATOR-ACCOUNT> --approval-prog ./stateful.teal --global-byteslices 4 --global-ints 4 --local-byteslices 0 --local-ints 0 --clear-prog ./clear.teal
We can then use the application ID returned to link the stateless contract to the stateful smart contract. The python file accepts an integer argument for the application ID which is passed to the PyTeal contract account:
python3 escrow.py <APP-ID> > escrow.teal
For the last step, we link the stateful smart contract to the stateless smart contract by using a NoOp transaction call to the application:
goal app call --app-id <APP-ID> --app-arg "str:set_escrow" --app-arg "addr:<ESCROW-ADDRESS>" -f <CREATOR-ACCOUNT>
The logic ensures that only the creator can set the escrow address and cannot reassign the value later. We do not want to allow one of the participants to change the escrow in the middle of a bet.
escrow = App.globalGetEx(Int(0), Bytes('escrow'))
on_set_escrow = Seq([
Assert(Global.group_size() == Int(1)),
Assert(Txn.sender() == Global.creator_address()),
escrow,
Assert(Not(escrow.hasValue())),
App.globalPut(Bytes('escrow'), Txn.application_args[1]),
Int(1)
])
Fund Escrow Account
All Algorand contracts have a minimum balance requirement of 100,000 microAlgos, which includes escrow accounts. Therefore, before we can submit any transactions from the escrow account we must first fund it.
There are also transaction fees. We could enforce the account that calls the application to pay for these fees (by grouping the requests with an Algo payment transaction to the escrow account) but to simplify the solution, we assume the escrow account has been sufficiently funded in advance.
To fund the escrow account, you can use the following goal command:
goal clerk send -a <AMOUNT> -f <ACCOUNT> -t <ESCROW-ADDRESS>
Implementation
We can divide the logic used in the PyTeal contracts into the following utility functions:
- flip the coin
- guess the outcome of the coin flip
- reveal the outcome of the coin flip
- claim winnings
Note: we use 1
for heads and 0
for tails.
Flip
The following occurs off-chain. The user flips a coin and gets a '1'
or '0'
which we will refer to as 'X'
. They then choose a random nonce and concatenate the outcome with the nonce and hash. For example, if we have tails and use nonce "test"
, then we hash "0test"
.
In this solution, we make use of sha256 hashing algorithm. The following command can be used to get the sha256 hash of "<X><NONCE>"
and base64 the value:
python3 -c "import hashlib;import base64;print(base64.b64encode(hashlib.sha256(str('<X><NONCE>').encode('utf-8')).digest()).decode('utf-8'))"
We can then pass the result as an argument to our stateful smart contract, along with the amount we are willing to bet, grouped in an atomic transfer:
# create transactions
goal app call --app-id <APP-ID> --app-arg "str:flip" --app-arg "b64:<HASH>" -f <FLIPPER-ACCOUNT> -o unsignedtx0.tx
goal clerk send -a <AMOUNT> -f <FLIPPER-ACCOUNT> -t <ESCROW-ADDRESS> -o unsignedtx1.tx
# combine transactions
cat unsignedtx0.tx unsignedtx1.tx > combinedtransactions.tx
# group transactions
goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
# split transactions
goal clerk split -i groupedtransactions.tx -o split.tx
# sign transactions
goal clerk sign -i split-0.tx -o signout-0.tx
goal clerk sign -i split-1.tx -o signout-1.tx
# assemble transaction group
cat signout-0.tx signout-1.tx > signout.tx
# submit
goal clerk rawsend -f signout.tx
We store the amount bet, the hash and the account that flipped the coin:
on_flip = Seq([
.
.
.
App.globalPut(Bytes('bet'), Gtxn[1].amount()),
App.globalPut(Bytes('secret'), Txn.application_args[1]),
App.globalPut(Bytes('flipper'), Txn.sender()),
.
.
.
Guess
After an account has flipped the coin, another account can guess the result. They simply submit their guess heads/tails (non-zero/zero), and match the bet already made:
goal app call --app-id <APP-ID> --app-arg "str:guess" --app-arg "int:<X>" -f <GUESSER-ACCOUNT> -o unsignedtx0.tx
goal clerk send -a <AMOUNT> -f <GUESSER-ACCOUNT> -t <ESCROW-ADDRESS> -o unsignedtx1.tx
# combine transactions
cat unsignedtx0.tx unsignedtx1.tx > combinedtransactions.tx
# group transactions
goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
# split transactions
goal clerk split -i groupedtransactions.tx -o split.tx
# sign transactions
goal clerk sign -i split-0.tx -o signout-0.tx
goal clerk sign -i split-1.tx -o signout-1.tx
# assemble transaction group
cat signout-0.tx signout-1.tx > signout.tx
# submit
goal clerk rawsend -f signout.tx
If the guess is correct, one concern is that the account that flipped the coin will never reveal whether it was a heads or tails. Therefore we set an expiry date from which the guesser can automatically claim the winnings if the flipper has not revealed the outcome. We also store the account that guessed the result and their guess:
one_day = Int(86400)
on_guess = Seq([
.
.
.
App.globalPut(Bytes('guess'), Btoi(Txn.application_args[1]) >= Int(1)),
App.globalPut(Bytes('guesser'), Txn.sender()),
App.globalPut(Bytes('expiry'), Global.latest_timestamp() + one_day),
Int(1)
])
Reveal
After an account has guessed the result of the coin flip, the flipper can reveal the result. This is done by submitting an application call transaction to the stateful smart contract with two arguments: heads/tails (non-zero/zero) and the secret nonce:
goal app call --app-id <APP-ID> --app-arg "str:reveal" --app-arg "int:<X>" --app-arg "str:<NONCE>" -f <FLIPPER-ACCOUNT>
We verify that the account is telling the truth by concatenating the revealed outcome with the nonce and checking if it matches the hash secret:
revealed = Btoi(Txn.application_args[1]) >= Int(1)
nonce = Txn.application_args[2]
preimage = Concat(Substring(Bytes('01'), revealed, revealed + Int(1)), nonce)
correct = Eq(App.globalGet(Bytes('secret')), Sha256(preimage))
If the values match then we store the result in the global state:
App.globalPut(Bytes('result'), revealed)
Claim
The winner can claim double their bet by grouping an application call transaction to the stateful smart contract with a payment from the escrow account:
# create transactions
goal app call --app-id <APP-ID> --app-arg "str:claim" -f <FLIPPER-ACCOUNT> -o unsignedtx0.tx
goal clerk send -a <AMOUNT> -f <ESCROW-ADDRESS> -t <FLIPPER-ACCOUNT> -o unsignedtx1.tx
# combine transactions
cat unsignedtx0.tx unsignedtx1.tx > combinedtransactions.tx
# group transactions
goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx
# split transactions
goal clerk split -i groupedtransactions.tx -o split.tx
# sign transactions
goal clerk sign -i split-0.tx -o signout-0.tx
goal clerk sign -i split-1.tx -p escrow.teal -o signout-1.tx
# assemble transaction group
cat signout-0.tx signout-1.tx > signout.tx
# submit
goal clerk rawsend -f signout.tx
The guesser can claim a win if the flipper has not revealed the outcome by the expiry date. Otherwise, we verify if the account claiming the money did indeed win the coin flip:
result = App.globalGetEx(Int(0), Bytes('result'))
on_claim = Seq([
.
.
.
If(
Not(result.hasValue()),
# if no reveal can claim if guesser and past expiry
Seq([
Assert(Txn.sender() == App.globalGet(Bytes('guesser'))),
Assert(Global.latest_timestamp() >= App.globalGet(Bytes('expiry')))
]),
# if have revealed then verify if won
Cond(
[
Txn.sender() == App.globalGet(Bytes('flipper')),
Assert(result.value() != App.globalGet(Bytes('guess')))
],
[
Txn.sender() == App.globalGet(Bytes('guesser')),
Assert(result.value() == App.globalGet(Bytes('guess')))
],
)
),
.
.
.
Conclusion
In this solution, we learned how to create a commitment scheme using a stateful smart contract and hashes. We used this for flipping a coin but the same pattern can be used in many other areas such as a sealed-bid auction and game show challenges.
The solution is limited in that it can only support one bet at a time. If we would like to scale, we may want to run individual coin flips on accounts’ local state, instead of using the global state. We may also want to require the winner to pay for the escrow transaction fee.