diff options
author | Ian Coleman <coleman.ian@gmail.com> | 2016-11-25 14:24:58 +1100 |
---|---|---|
committer | Ian Coleman <coleman.ian@gmail.com> | 2016-11-25 14:24:58 +1100 |
commit | 886f06ee6bbca2e6e05109dbd7e264bdea35238f (patch) | |
tree | 89d7d36e913c497516347b20e7818ad9dd804f25 | |
parent | 9e97eb7684bf2c2d9c5276561f29fe50caf31f82 (diff) | |
download | BIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.tar.gz BIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.tar.zst BIP39-886f06ee6bbca2e6e05109dbd7e264bdea35238f.zip |
Card entropy calculation bugfix
See https://github.com/iancoleman/bip39/issues/33
-rw-r--r-- | src/js/entropy.js | 96 | ||||
-rw-r--r-- | tests.js | 182 |
2 files changed, 260 insertions, 18 deletions
diff --git a/src/js/entropy.js b/src/js/entropy.js index f41cf80..8a0c799 100644 --- a/src/js/entropy.js +++ b/src/js/entropy.js | |||
@@ -16,6 +16,8 @@ | |||
16 | 16 | ||
17 | window.Entropy = new (function() { | 17 | window.Entropy = new (function() { |
18 | 18 | ||
19 | var TWO = new BigInteger(2); | ||
20 | |||
19 | // matchers returns an array of the matched events for each type of entropy. | 21 | // matchers returns an array of the matched events for each type of entropy. |
20 | // eg | 22 | // eg |
21 | // matchers.binary("010") returns ["0", "1", "0"] | 23 | // matchers.binary("010") returns ["0", "1", "0"] |
@@ -124,7 +126,6 @@ window.Entropy = new (function() { | |||
124 | // eg the second last card can be only one of two, not one of fifty two | 126 | // eg the second last card can be only one of two, not one of fifty two |
125 | // so the added entropy for that card is only one bit at most | 127 | // so the added entropy for that card is only one bit at most |
126 | if (base.asInt == 52) { | 128 | if (base.asInt == 52) { |
127 | // Get the maximum value WITHOUT replacement | ||
128 | var totalDecks = Math.ceil(base.parts.length / 52); | 129 | var totalDecks = Math.ceil(base.parts.length / 52); |
129 | var totalCards = totalDecks * 52; | 130 | var totalCards = totalDecks * 52; |
130 | var totalCombos = factorial(52).pow(totalDecks); | 131 | var totalCombos = factorial(52).pow(totalDecks); |
@@ -135,18 +136,77 @@ window.Entropy = new (function() { | |||
135 | var currentCombos = totalCombos.divide(remainingCombos); | 136 | var currentCombos = totalCombos.divide(remainingCombos); |
136 | var numberOfBits = Math.log2(currentCombos); | 137 | var numberOfBits = Math.log2(currentCombos); |
137 | var maxWithoutReplace = BigInteger.pow(2, numberOfBits); | 138 | var maxWithoutReplace = BigInteger.pow(2, numberOfBits); |
138 | // aggresive flooring of numberOfBits by BigInteger.pow means a | 139 | // Use a bunch of sorted decks to measure entropy from, populated |
139 | // more accurate result can be had for small numbers using the | 140 | // as needed. |
140 | // built-in Math.pow function. | 141 | var sortedDecks = []; |
141 | if (numberOfBits < 32) { | 142 | // Initialize the final entropy value for these cards |
142 | maxWithoutReplace = BigInteger(Math.round(Math.pow(2, numberOfBits))); | 143 | var entropyInt = BigInteger.ZERO; |
144 | // Track how many instances of each card have been used, and thus | ||
145 | // how many decks are in use. | ||
146 | var cardCounts = {}; | ||
147 | // Track the total bits of entropy that remain, which diminishes as | ||
148 | // each card is drawn. | ||
149 | var totalBitsLeft = numberOfBits; | ||
150 | // Work out entropy contribution of each card drawn | ||
151 | for (var i=0; i<base.parts.length; i++) { | ||
152 | // Get the card that was drawn | ||
153 | var cardLower = base.parts[i]; | ||
154 | var card = cardLower.toUpperCase(); | ||
155 | // Initialize the deck for this card if needed, to track how | ||
156 | // much entropy it adds. | ||
157 | if (!(card in cardCounts)) { | ||
158 | cardCounts[card] = 0; | ||
159 | } | ||
160 | // Get the deck this card is from | ||
161 | var deckIndex = cardCounts[card]; | ||
162 | while (deckIndex > sortedDecks.length-1) { | ||
163 | sortedDecks.push(getSortedDeck()); | ||
164 | } | ||
165 | // See how many bits this card contributes (depends on how many | ||
166 | // are left in the deck it's from) | ||
167 | var deckForCard = sortedDecks[deckIndex]; | ||
168 | var cardsLeftInDeck = deckForCard.length; | ||
169 | var additionalBits = Math.log2(cardsLeftInDeck); | ||
170 | // Work out the min and max value for this card | ||
171 | var nextTotalBitsLeft = totalBitsLeft - additionalBits; | ||
172 | var minPossibleNewEntropy = TWO.pow(nextTotalBitsLeft).subtract(1); | ||
173 | var maxPossibleNewEntropy = TWO.pow(totalBitsLeft).subtract(1); | ||
174 | var diff = maxPossibleNewEntropy.subtract(minPossibleNewEntropy); | ||
175 | // BigInteger aggresively floors numbers which greatly affects | ||
176 | // the small numbers. In that case, use native Math library | ||
177 | var useBigInt = totalBitsLeft >= 32; | ||
178 | if (!useBigInt) { | ||
179 | minPossibleNewEntropy = Math.round(Math.pow(2, nextTotalBitsLeft)-1); | ||
180 | maxPossibleNewEntropy = Math.round(Math.pow(2, totalBitsLeft)-1); | ||
181 | diff = maxPossibleNewEntropy - minPossibleNewEntropy; | ||
182 | } | ||
183 | // Scale the value between possible min and max depending on | ||
184 | // this card value | ||
185 | var thisCardIndex = deckForCard.indexOf(card); | ||
186 | var toAdd = BigInteger.ZERO; | ||
187 | if (cardsLeftInDeck > 1) { | ||
188 | if (useBigInt) { | ||
189 | toAdd = diff.multiply(thisCardIndex) | ||
190 | .divide(deckForCard.length - 1) | ||
191 | .add(minPossibleNewEntropy); | ||
192 | } | ||
193 | else { | ||
194 | var ratio = thisCardIndex / (deckForCard.length -1); | ||
195 | var f = diff * ratio; | ||
196 | toAdd = new BigInteger(f).add(minPossibleNewEntropy); | ||
197 | } | ||
198 | } | ||
199 | // Add this card entropy to existing entropy | ||
200 | entropyInt = entropyInt.add(toAdd); | ||
201 | // Remove this card from the deck it comes from | ||
202 | deckForCard.splice(thisCardIndex,1); | ||
203 | // Ensure the next insance of this card uses the next deck | ||
204 | cardCounts[card] = cardCounts[card] + 1; | ||
205 | // Next card drawn has less total remaining bits to work with | ||
206 | totalBitsLeft = nextTotalBitsLeft; | ||
143 | } | 207 | } |
144 | // Get the maximum value WITH replacement | 208 | // Convert to binary |
145 | var maxWithReplace = BigInteger.pow(52, base.parts.length); | 209 | var entropyBin = entropyInt.toString(2); |
146 | // Calculate the new value by scaling the original value down | ||
147 | var withoutReplace = entropyInt.multiply(maxWithoutReplace).divide(maxWithReplace); | ||
148 | // Left pad with zeros based on number of bits | ||
149 | var entropyBin = withoutReplace.toString(2); | ||
150 | var numberOfBitsInt = Math.floor(numberOfBits); | 210 | var numberOfBitsInt = Math.floor(numberOfBits); |
151 | while (entropyBin.length < numberOfBitsInt) { | 211 | while (entropyBin.length < numberOfBitsInt) { |
152 | entropyBin = "0" + entropyBin; | 212 | entropyBin = "0" + entropyBin; |
@@ -177,6 +237,18 @@ window.Entropy = new (function() { | |||
177 | return e; | 237 | return e; |
178 | } | 238 | } |
179 | 239 | ||
240 | function getSortedDeck() { | ||
241 | var s = []; | ||
242 | var suits = "CDHS"; | ||
243 | var values = "A23456789TJQK"; | ||
244 | for (var i=0; i<suits.length; i++) { | ||
245 | for (var j=0; j<values.length; j++) { | ||
246 | s.push(values[j]+suits[i]); | ||
247 | } | ||
248 | } | ||
249 | return s; | ||
250 | } | ||
251 | |||
180 | function getBase(str) { | 252 | function getBase(str) { |
181 | // Need to get the lowest base for the supplied entropy. | 253 | // Need to get the lowest base for the supplied entropy. |
182 | // This prevents interpreting, say, dice rolls as hexadecimal. | 254 | // This prevents interpreting, say, dice rolls as hexadecimal. |
@@ -2185,9 +2185,9 @@ page.open(url, function(status) { | |||
2185 | try { | 2185 | try { |
2186 | var cards = [ | 2186 | var cards = [ |
2187 | [ "ac", "00000" ], | 2187 | [ "ac", "00000" ], |
2188 | [ "acqs", "00000110001" ], | 2188 | [ "acqs", "00001100011" ], |
2189 | [ "acks", "00000110010" ], | 2189 | [ "acks", "00001100100" ], |
2190 | [ "2cac", "00000110011" ], | 2190 | [ "2cac", "00001100101" ], |
2191 | [ "2c", "00001" ], | 2191 | [ "2c", "00001" ], |
2192 | [ "3d", "01111" ], | 2192 | [ "3d", "01111" ], |
2193 | [ "4h", "11101" ], | 2193 | [ "4h", "11101" ], |
@@ -2200,8 +2200,8 @@ page.open(url, function(status) { | |||
2200 | [ "jd", "10111" ], | 2200 | [ "jd", "10111" ], |
2201 | [ "qh", "100101" ], | 2201 | [ "qh", "100101" ], |
2202 | [ "ks", "110011" ], | 2202 | [ "ks", "110011" ], |
2203 | [ "ks2c", "101000101001" ], | 2203 | [ "ks2c", "101001011100" ], |
2204 | [ "KS2C", "101000101001" ], | 2204 | [ "KS2C", "101001011100" ], |
2205 | ]; | 2205 | ]; |
2206 | for (var i=0; i<cards.length; i++) { | 2206 | for (var i=0; i<cards.length; i++) { |
2207 | var card = cards[i][0]; | 2207 | var card = cards[i][0]; |
@@ -2209,7 +2209,7 @@ page.open(url, function(status) { | |||
2209 | e = Entropy.fromString(card); | 2209 | e = Entropy.fromString(card); |
2210 | console.log(e.binary + " " + result); | 2210 | console.log(e.binary + " " + result); |
2211 | if (e.binaryStr !== result) { | 2211 | if (e.binaryStr !== result) { |
2212 | return "card entropy not parsed correctly: " + result + " != " + e.binaryStr; | 2212 | return "card entropy " + card + " not parsed correctly: " + result + " != " + e.binaryStr; |
2213 | } | 2213 | } |
2214 | } | 2214 | } |
2215 | } | 2215 | } |
@@ -2724,6 +2724,136 @@ page.open(url, function(status) { | |||
2724 | words: 18, | 2724 | words: 18, |
2725 | strength: "extremely strong", | 2725 | strength: "extremely strong", |
2726 | }, | 2726 | }, |
2727 | // Additional entropy from each card decreases as deck is depleted. | ||
2728 | // Check the boundaries of this depletion | ||
2729 | // See table at https://github.com/iancoleman/bip39/issues/33#issuecomment-262855862 | ||
2730 | // for following values of 'events, bits, words' | ||
2731 | // 2 cards remaining = 21 words | ||
2732 | { | ||
2733 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjs", | ||
2734 | type: "card (2 missing: QS KS)", | ||
2735 | events: 50, | ||
2736 | bits: 224, | ||
2737 | words: 21, | ||
2738 | strength: "extremely strong", | ||
2739 | }, | ||
2740 | // 3 cards remaining = 18 words | ||
2741 | { | ||
2742 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9sts", | ||
2743 | type: "card (3 missing: JS QS KS)", | ||
2744 | events: 49, | ||
2745 | bits: 222, // table uses different rounding - 222.99604 | ||
2746 | words: 18, | ||
2747 | strength: "extremely strong", | ||
2748 | }, | ||
2749 | // 13 cards remaining = 18 words | ||
2750 | { | ||
2751 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkh", | ||
2752 | type: "card", | ||
2753 | events: 39, | ||
2754 | bits: 193, | ||
2755 | words: 18, | ||
2756 | strength: "extremely strong", | ||
2757 | }, | ||
2758 | // 14 cards remaining = 15 words | ||
2759 | { | ||
2760 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqh", | ||
2761 | type: "card", | ||
2762 | events: 38, | ||
2763 | bits: 189, | ||
2764 | words: 15, | ||
2765 | strength: "very strong", | ||
2766 | }, | ||
2767 | // 21 cards remaining = 15 words | ||
2768 | { | ||
2769 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h", | ||
2770 | type: "card", | ||
2771 | events: 31, | ||
2772 | bits: 160, | ||
2773 | words: 15, | ||
2774 | strength: "very strong", | ||
2775 | }, | ||
2776 | // 22 cards remaining = 12 words | ||
2777 | { | ||
2778 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h", | ||
2779 | type: "card", | ||
2780 | events: 30, | ||
2781 | bits: 155, | ||
2782 | words: 12, | ||
2783 | strength: "strong", | ||
2784 | }, | ||
2785 | // 27 cards remaining = 12 words | ||
2786 | { | ||
2787 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqd", | ||
2788 | type: "card", | ||
2789 | events: 25, | ||
2790 | bits: 132, | ||
2791 | words: 12, | ||
2792 | strength: "strong", | ||
2793 | }, | ||
2794 | // 28 cards remaining = 9 words | ||
2795 | { | ||
2796 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjd", | ||
2797 | type: "card", | ||
2798 | events: 24, | ||
2799 | bits: 127, | ||
2800 | words: 9, | ||
2801 | strength: "weak", | ||
2802 | }, | ||
2803 | // 34 cards remaining = 9 words | ||
2804 | { | ||
2805 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d", | ||
2806 | type: "card", | ||
2807 | events: 18, | ||
2808 | bits: 97, | ||
2809 | words: 9, | ||
2810 | strength: "weak", | ||
2811 | }, | ||
2812 | // 35 cards remaining = 6 words | ||
2813 | { | ||
2814 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d", | ||
2815 | type: "card", | ||
2816 | events: 17, | ||
2817 | bits: 92, | ||
2818 | words: 6, | ||
2819 | strength: "very weak", | ||
2820 | }, | ||
2821 | // 40 cards remaining = 6 words | ||
2822 | { | ||
2823 | entropy: "ac2c3c4c5c6c7c8c9ctcjcqc", | ||
2824 | type: "card", | ||
2825 | events: 12, | ||
2826 | bits: 66, | ||
2827 | words: 6, | ||
2828 | strength: "very weak", | ||
2829 | }, | ||
2830 | // 41 cards remaining = 3 words | ||
2831 | { | ||
2832 | entropy: "ac2c3c4c5c6c7c8c9ctcjc", | ||
2833 | type: "card", | ||
2834 | events: 11, | ||
2835 | bits: 61, | ||
2836 | words: 3, | ||
2837 | strength: "extremely weak", | ||
2838 | }, | ||
2839 | // 46 cards remaining = 3 words | ||
2840 | { | ||
2841 | entropy: "ac2c3c4c5c6c", | ||
2842 | type: "card", | ||
2843 | events: 6, | ||
2844 | bits: 33, | ||
2845 | words: 3, | ||
2846 | strength: "extremely weak", | ||
2847 | }, | ||
2848 | // 47 cards remaining = 0 words | ||
2849 | { | ||
2850 | entropy: "ac2c3c4c5c", | ||
2851 | type: "card", | ||
2852 | events: 5, | ||
2853 | bits: 28, | ||
2854 | words: 0, | ||
2855 | strength: "extremely weak", | ||
2856 | }, | ||
2727 | ]; | 2857 | ]; |
2728 | // use entropy | 2858 | // use entropy |
2729 | page.evaluate(function() { | 2859 | page.evaluate(function() { |
@@ -2946,6 +3076,46 @@ page.open(url, function(status) { | |||
2946 | }); | 3076 | }); |
2947 | }, | 3077 | }, |
2948 | 3078 | ||
3079 | // Github issue 33 | ||
3080 | // https://github.com/iancoleman/bip39/issues/33 | ||
3081 | // Final cards should contribute entropy | ||
3082 | function() { | ||
3083 | page.open(url, function(status) { | ||
3084 | // use entropy | ||
3085 | page.evaluate(function() { | ||
3086 | $(".use-entropy").prop("checked", true).trigger("change"); | ||
3087 | $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-6C TS TH 4H KC 5H 2H AH 2C 8D 3H 5D").trigger("input"); | ||
3088 | }); | ||
3089 | // get the mnemonic | ||
3090 | waitForGenerate(function() { | ||
3091 | var originalPhrase = page.evaluate(function() { | ||
3092 | return $(".phrase").val(); | ||
3093 | }); | ||
3094 | // Set the last 12 cards to be AS | ||
3095 | page.evaluate(function() { | ||
3096 | $(".addresses").empty(); | ||
3097 | $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-AS AS AS AS AS AS AS AS AS AS AS AS").trigger("input"); | ||
3098 | }); | ||
3099 | // get the new mnemonic | ||
3100 | waitForGenerate(function() { | ||
3101 | var newPhrase = page.evaluate(function() { | ||
3102 | return $(".phrase").val(); | ||
3103 | }); | ||
3104 | if (newPhrase == originalPhrase) { | ||
3105 | console.log("Changing last 12 cards does not change mnemonic"); | ||
3106 | console.log("Original:"); | ||
3107 | console.log(originalPhrase); | ||
3108 | console.log("New:"); | ||
3109 | console.log(newPhrase); | ||
3110 | fail(); | ||
3111 | } | ||
3112 | next(); | ||
3113 | }); | ||
3114 | }); | ||
3115 | }); | ||
3116 | }, | ||
3117 | |||
3118 | |||
2949 | // If you wish to add more tests, do so here... | 3119 | // If you wish to add more tests, do so here... |
2950 | 3120 | ||
2951 | // Here is a blank test template | 3121 | // Here is a blank test template |