Skip to content

Commit

Permalink
Use ReverseWordMap to make O(1) lookups for IsMnemonicValid (#2)
Browse files Browse the repository at this point in the history
Noticed while auditing a dependency of Cosmos-sdk code from
PR cosmos/cosmos-sdk#8099, WordList
is loaded at init time, and so is ReverseWordMap.
Usually a slice is sufficient for doing linear lookups for
the existence of a string. However, given that IsMnemonicValid
requires word segements with 12 to 24 lengths, this means that
2048 * ~12 lookups in the worst case, and given that we already
at init time load up ReverseWordMap, it is important for us
perform a constant time lookup. The data also backs up the speed up
with the benchmark results below:

```shell
name               old time/op    new time/op    delta
IsMnemonicValid-8    24.3µs ± 1%     1.3µs ± 1%  -94.64%  (p=0.000 n=9+10)

name               old alloc/op   new alloc/op   delta
IsMnemonicValid-8      576B ± 0%      576B ± 0%     ~     (all equal)

name               old allocs/op  new allocs/op  delta
IsMnemonicValid-8      3.00 ± 0%      3.00 ± 0%     ~     (all equal)
```
  • Loading branch information
odeke-em committed Dec 9, 2020
1 parent 802d6af commit bdfa7c8
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 15 deletions.
26 changes: 26 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package bip39

import "testing"

var words = []string{
"wolf afraid artwork blanket carpet cricket wolf afraid artwork blanket carpet cricket",
"artwork blanket carpet cricket disorder disorder artwork blanket carpet cricket disorder disorder",
"carpet cricket disorder cricket cricket artwork carpet cricket disorder cricket cricket artwork ",
}

func BenchmarkIsMnemonicValid(b *testing.B) {
b.ReportAllocs()
var sharp interface{}
for i := 0; i < b.N; i++ {
for _, word := range words {
ok := IsMnemonicValid(word)
if !ok {
b.Fatal("returned false")
}
sharp = ok
}
}
if sharp == nil {
b.Fatal("benchmark was not run")
}
}
19 changes: 4 additions & 15 deletions bip39.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func MnemonicToByteArray(mnemonic string) ([]byte, error) {
modulo := big.NewInt(2048)
for _, v := range mnemonicSlice {
index, found := ReverseWordMap[v]
if found == false {
if !found {
return nil, fmt.Errorf("Word `%v` not found in reverse map", v)
}
add := big.NewInt(int64(index))
Expand Down Expand Up @@ -176,9 +176,7 @@ func NewSeed(mnemonic string, password string) []byte {
// Currently only supports data up to 32 bytes
func addChecksum(data []byte) []byte {
// Get first byte of sha256
hasher := sha256.New()
hasher.Write(data)
hash := hasher.Sum(nil)
hash := sha256.Sum256(data)
firstChecksumByte := hash[0]

// len() is in bytes so we divide by 4
Expand Down Expand Up @@ -231,25 +229,16 @@ func IsMnemonicValid(mnemonic string) bool {
numOfWords := len(words)

// The number of words should be 12, 15, 18, 21 or 24
if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 {
if numOfWords < 12 || numOfWords > 24 || numOfWords%3 != 0 {
return false
}

// Check if all words belong in the wordlist
for i := 0; i < numOfWords; i++ {
if !contains(WordList, words[i]) {
if _, ok := ReverseWordMap[words[i]]; !ok {
return false
}
}

return true
}

func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}

0 comments on commit bdfa7c8

Please sign in to comment.