Skip to content

Commit

Permalink
FEATURE Initial commit of a provably fair dice implementation.
Browse files Browse the repository at this point in the history
Larging spec'd based on http://dicesites.com/provably-fair
  • Loading branch information
tyler-smith committed Oct 4, 2015
0 parents commit b8fb4e9
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 0 deletions.
144 changes: 144 additions & 0 deletions dice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package dice

import (
"encoding/hex"
"errors"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"strconv"
"sync"
)

// ErrClientSeedBlank is the error returned if the supplied clientSeed is nil
// or a slice of 0 length
var ErrClientSeedBlank = errors.New("Client seed can't be empty")

// ErrInvalidNonce is returned when doesn't create a valid random number
var ErrInvalidNonce = errors.New("Invalid nonce")

// Game reprsents the current state of a dice game for a single client
type Game struct {
ClientSeed []byte
ServerSeed []byte
BlindedServerSeed []byte

Nonce uint64

RollLock sync.Mutex
}

// NewGame creates a new game from the given seeds. A clientSeed is required
// If the serverSeed is nil we create a random one
func NewGame(clientSeed []byte, serverSeed []byte) (*Game, error) {
// Validate the clientSeed
if clientSeed == nil || len(clientSeed) == 0 {
return nil, ErrClientSeedBlank
}

// Generate a random server seed if one isn't provided
if serverSeed == nil || len(serverSeed) == 0 {
var err error
serverSeed, err = newServerSeed(32)
if err != nil {
return nil, err
}
}

// Hash the serverSeed to show the client
blindedSeed := sha256.Sum256(serverSeed)

return &Game{
Nonce: 0,
ClientSeed: clientSeed,
ServerSeed: serverSeed,
BlindedServerSeed: blindedSeed[:],
}, nil
}

// Roll calculates the number for the current nonce, then increments the nonce
// Doing it in this order ensures that the first once we use is 0
func (g *Game) Roll() (float64, error) {
// Lock the RollLock so we can be safe across threads accessing the same game
g.RollLock.Lock()
defer g.RollLock.Unlock()

// Calculate the current number from the current state
roll, err := g.Calculate()
if err != nil {
return roll, err
}

// Increment the nonce for next time
g.Nonce += 1

return roll, nil
}

// Calculate calculates the current value from the current state of the game
// It does not advance the state in anyway; i.e. simply calling Calculate
// multiple times will always result in the same value unless the Nonce changes
func (g *Game) Calculate() (float64, error) {
// Calculate the HMAC for the current nonce
ourHMAC := string(g.CalculateHMAC())

// Find the first 5 character segment that converts to decimal < the max
var randNum uint64
var err error
for i := 0; i < len(ourHMAC)-5; i++ {
// Get the index for this segment and ensure it doesn't overrun the slice
idx := i * 5
if len(ourHMAC) < (idx + 5) {
break
}

// Get 5 characters and convert them to decimal
randNum, err = strconv.ParseUint(ourHMAC[idx:idx+5], 16, 0)
if err != nil {
return 0, err
}

// Continue unless our number was greater than our max
if randNum <= 999999 {
break
}
}

// If even the last segment was invalid we must give up
if randNum > 999999 {
return 0, ErrInvalidNonce
}

// Normalize the number to [0,100]
return float64(randNum%10000)/100, nil
}

// CalculateHMAC calculates the hmac of "clientseed-nonce" as a hex string
func (g *Game) CalculateHMAC() []byte {
h := hmac.New(sha512.New, g.ServerSeed)
h.Write(append(append(g.ClientSeed, '-'), []byte(strconv.FormatUint(g.Nonce, 10))...))

ourHMAC := make([]byte, 128)
hex.Encode(ourHMAC, h.Sum(nil))
return ourHMAC
}

// Verfiy takes a state and checks that the supplied number was fairly generated
func Verfiy(clientSeed []byte, serverSeed []byte, nonce uint64, randNum float64) (bool, error) {
game, _ := NewGame(clientSeed, serverSeed)
game.Nonce = nonce

roll, err := game.Calculate()
if err != nil {
return false, err
}

return roll == randNum, nil
}

func newServerSeed(byteCount int) ([]byte, error) {
seed := make([]byte, byteCount)
_, err := rand.Read(seed)
return seed, err
}
99 changes: 99 additions & 0 deletions dice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package dice

import (
"fmt"
"encoding/hex"
"github.com/stretchr/testify/assert"
"testing"
)

type testRollExpectations struct {
*Game

Nonce uint64
HMAC []byte
Roll float64
}

var (
ExampleClientSeed = []byte("ClientSeedForDiceSites.com")
ExampleServerSeed = []byte("293d5d2ddd365f54759283a8097ab2640cbe6f8864adc2b1b31e65c14c999f04")
)

func TestGame(t *testing.T) {
for _, testRoll := range newTestRolls(t) {
assert.Equal(t, testRoll.Nonce, testRoll.Game.Nonce)
assert.Equal(t, testRoll.HMAC, testRoll.Game.CalculateHMAC())

roll, err := testRoll.Game.Roll()
assert.NoError(t, err)
assert.Equal(t, testRoll.Roll, roll)

if testing.Verbose() {
fmt.Println("Client Seed:", string(testRoll.ClientSeed))
fmt.Println("Server Seed:", string(testRoll.ServerSeed))
fmt.Println("Blinded Server Seed Hex:", hex.EncodeToString(testRoll.BlindedServerSeed))
fmt.Println("Nonce:", testRoll.Nonce)
fmt.Println("HMAC:", string(testRoll.HMAC))
fmt.Println("Roll:", roll)
}
}
}

func TestRandomServerSeed(t *testing.T) {
game, err := NewGame(ExampleClientSeed, nil)
assert.NoError(t, err)
assert.Equal(t, uint64(0), game.Nonce)
assert.Equal(t, ExampleClientSeed, game.ClientSeed)
assert.Equal(t, 32, len(game.ServerSeed))
assert.Equal(t, 32, len(game.BlindedServerSeed))
}

func TestVerify(t *testing.T) {
for _, testRoll := range newTestRolls(t) {
verified, err := Verfiy(testRoll.ClientSeed, testRoll.ServerSeed, testRoll.Nonce, testRoll.Roll)
assert.NoError(t, err)
assert.True(t, verified)
}
}

func newExampleGame(t *testing.T) *Game {
game, err := NewGame(ExampleClientSeed, ExampleServerSeed)
assert.NoError(t, err)
assert.Equal(t, uint64(0), game.Nonce)
assert.Equal(t, ExampleClientSeed, game.ClientSeed)
assert.Equal(t, ExampleServerSeed, game.ServerSeed)
assert.Equal(t, 32, len(game.BlindedServerSeed))
return game
}

func newTestRolls(t *testing.T) []*testRollExpectations {
game := newExampleGame(t)
return []*testRollExpectations{
{
Game: game,
Nonce: 0,
HMAC: []byte("aa671aad5e4565ebffb8dc5c185e4df1ae6d9aca2578b5c03ec9c7750f881922276d8044e5e3d84f158ce411f667e224e9b0c1ac50fc94e9c5eb883a678f6ca2"),
Roll: 79.69,
},
{
Game: game,
Nonce: 1,
HMAC: []byte("7b9062b1a8188feff82d643c0c8f2883bc744240594952f55126b24c76b05648a73850905e68fe86fe64c9fbd9a9ef9f677264d3771bd98db64b022ad183da53"),
Roll: 61.18,
},
{
Game: game,
Nonce: 2,
HMAC: []byte("a5644976f61b4012c0eb27848bbe3d05d43d34dcb89e2032b8d93ba0992b26ad916223caf9ba5421229508144a370ba053f27893b5e7f6e8283231cce90e1535"),
Roll: 74.44,
},
{
Game: game,
Nonce: 3,
HMAC: []byte("8bd6805955d0ca66fb5eb672b75bd0874bea59ecbe1d21e101ad50faf19e7d67256d6d4714c53fa848d801d92874f72813a78e447431b1fd609ba328d18d3875"),
Roll: 27.76,
},
}

}

0 comments on commit b8fb4e9

Please sign in to comment.