-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
FEATURE Initial commit of a provably fair dice implementation.
Larging spec'd based on http://dicesites.com/provably-fair
- Loading branch information
0 parents
commit b8fb4e9
Showing
2 changed files
with
243 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
} | ||
|
||
} |