Skip to content

Commit 6de0630

Browse files
committed
Add generation of mnemonic from dice rolls
1 parent c0b7d57 commit 6de0630

File tree

2 files changed

+165
-1
lines changed

2 files changed

+165
-1
lines changed

buidl/mnemonic.py

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from os import path
22
from secrets import randbits
33
from time import time
4+
from math import ceil
45

56
from buidl.helper import big_endian_to_int, int_to_big_endian, sha256
67

@@ -98,6 +99,40 @@ def bytes_to_mnemonic(b, num_bits):
9899
return " ".join(mnemonic)
99100

100101

102+
def dice_rolls_to_mnemonic(dice_rolls, num_words=24, allow_low_entropy=False):
103+
"""
104+
returns a mnemonic from a string of 6-sided dice rolls
105+
(>=100 rolls for 24 words recommended)
106+
(>=50 rolls for 12 words recommended)
107+
"""
108+
# check that the number of words provided is 12, 15, 18, 21, or 24
109+
if num_words not in (12, 15, 18, 21, 24):
110+
raise InvalidBIP39Length(
111+
f"{num_words} words requested (must be 12, 15, 18, 21, or 24 words)"
112+
)
113+
# check that valid dice rolls have been provided
114+
if not isinstance(dice_rolls, str):
115+
raise ValueError("Dice rolls must be provided as a string")
116+
if (
117+
len([roll for roll in dice_rolls if roll not in ("1", "2", "3", "4", "5", "6")])
118+
> 0
119+
):
120+
raise ValueError("Dice roll string contained invalid dice numbers")
121+
# entropy (in bits) per 6-sided dice roll (i.e. log2(6)=2.585)
122+
entropy_per_roll = 2.585
123+
mnemonic_entropy = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
124+
# check that the appropriate amount of entropy had been provided
125+
min_rolls_needed = ceil(mnemonic_entropy[num_words] / entropy_per_roll)
126+
if not allow_low_entropy and (len(dice_rolls) < min_rolls_needed):
127+
raise ValueError(
128+
f"Received {len(dice_rolls)} rolls but need at least {min_rolls_needed}"
129+
f" rolls (for {mnemonic_entropy[num_words]} bits of entropy)"
130+
)
131+
kept_bytes = mnemonic_entropy[num_words] // 8
132+
rolls_hash = sha256(dice_rolls.encode())[:kept_bytes]
133+
return bytes_to_mnemonic(rolls_hash, kept_bytes * 8)
134+
135+
101136
class WordList:
102137
def __init__(self, filename, num_words):
103138
word_file = path.join(path.dirname(__file__), filename)

buidl/test/test_mnemonic.py

+130-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from unittest import TestCase
22

3-
from buidl.mnemonic import secure_mnemonic
3+
from buidl.mnemonic import (
4+
secure_mnemonic,
5+
dice_rolls_to_mnemonic,
6+
InvalidBIP39Length,
7+
)
48
from buidl.hd import HDPrivateKey
59

610

@@ -45,3 +49,128 @@ def test_secure_mnemonic_extra_entropy(self):
4549
secure_mnemonic(extra_entropy="not an int")
4650
with self.assertRaises(ValueError):
4751
secure_mnemonic(extra_entropy=-1)
52+
53+
def test_dice_to_mnemonic(self):
54+
tests = [ # dice_rolls, num_words, allow_low_entropy, expected_mnemonic, expected_exception
55+
[
56+
"",
57+
24,
58+
True,
59+
"together mail awful cradle scrub apart hip leader silk slice unusual embark kit can muscle nature nation gown century cram resource citizen throw produce",
60+
None,
61+
],
62+
[
63+
"123456",
64+
24,
65+
True,
66+
"mirror reject rookie talk pudding throw happy era myth already payment own sentence push head sting video explain letter bomb casual hotel rather garment",
67+
None,
68+
],
69+
[
70+
"123456123456123456123456123456123456123456123456123456123456123456123456123456123456123456123456123456",
71+
24,
72+
False,
73+
"more matter caught bind tip twin indicate visa rifle angle defense lizard stock cave cradle injury always mule photo horse range opinion affair garlic",
74+
None,
75+
],
76+
[
77+
"523365252662366",
78+
24,
79+
True,
80+
"dilemma rural physical exhaust divorce escape nut umbrella lawn midnight prosper prevent employ caught mercy student arctic umbrella feed super mad magic crawl fiscal",
81+
None,
82+
],
83+
[
84+
"",
85+
12,
86+
True,
87+
"together mail awful cradle scrub apart hip leader silk slice unusual embark",
88+
None,
89+
],
90+
[
91+
"123456",
92+
12,
93+
True,
94+
"mirror reject rookie talk pudding throw happy era myth already payment owner",
95+
None,
96+
],
97+
[
98+
"12345612345612345612345612345612345612345612345612",
99+
12,
100+
False,
101+
"unveil nice picture region tragic fault cream strike tourist control recipe tourist",
102+
None,
103+
],
104+
[
105+
"12345612345612345612345612345612345612345612345612345612345612",
106+
15,
107+
False,
108+
"end spider topple cliff tomorrow process dismiss produce athlete film monster team vacant ill silk",
109+
None,
110+
],
111+
[
112+
"123456123456123456123456123456123456123456123456123456123456123456123456123",
113+
18,
114+
False,
115+
"melt churn alley retreat flip once enough gather project prosper cannon nasty furnace isolate cost laundry lottery slice",
116+
None,
117+
],
118+
[
119+
"123456123456123456123456123456123456123456123456123456123456123456123456123456123456123",
120+
21,
121+
False,
122+
"start insane amazing fall kite punch owner refuse bone trigger spirit luggage slide sound reopen broom remember nose limb swallow kitten",
123+
None,
124+
],
125+
[ # low entropy not allowed
126+
"123456",
127+
24,
128+
False,
129+
"",
130+
ValueError(
131+
"Received 6 rolls but need at least 100 rolls (for 256 bits of entropy)"
132+
),
133+
],
134+
[ # Invalid num_words
135+
"123456",
136+
23,
137+
False,
138+
"",
139+
InvalidBIP39Length(
140+
"23 words requested (must be 12, 15, 18, 21, or 24 words)"
141+
),
142+
],
143+
[ # non-string dice rolls
144+
b"123456",
145+
24,
146+
False,
147+
"",
148+
ValueError("Dice rolls must be provided as a string"),
149+
],
150+
[ # string containing non-dice values
151+
"1234567",
152+
24,
153+
False,
154+
"",
155+
ValueError("Dice roll string contained invalid dice numbers"),
156+
],
157+
]
158+
159+
for (
160+
dice_rolls,
161+
num_words,
162+
allow_low_entropy,
163+
expected_mnemonic,
164+
expected_exception,
165+
) in tests:
166+
if expected_exception is None:
167+
received_mnemonic = dice_rolls_to_mnemonic(
168+
dice_rolls, num_words, allow_low_entropy
169+
)
170+
self.assertEqual(received_mnemonic, expected_mnemonic)
171+
else:
172+
with self.assertRaises(type(expected_exception)) as exception_context:
173+
dice_rolls_to_mnemonic(dice_rolls, num_words, allow_low_entropy)
174+
self.assertEqual(
175+
str(exception_context.exception), str(expected_exception)
176+
)

0 commit comments

Comments
 (0)