What are some interesting things we can do with the BIP-39 mnemonic generator we built in a previous article? The most obvious answer would be to create seeds for Bitcoin wallets, but I’m not looking for obvious today.
What if we sift through the generated mnemonics looking for structurally sound haiku? That sounds like a good time and a great excuse to play with Elixir!
Put on your lab coat and grab your Morty, we’re going full mad scientist today!
Mining for Haiku
If you’re not familiar, haiku are poems with a strictly defined structure. Haiku are three lines in length. The first and last lines are five syllables long, and the middle line is seven syllables in length.
Haiku traditionally focus on nature and the juxtaposition of two contrasting idea. Our haiku… will not.
exhibit horror
make obscure arrive unveil
detail law pig prize
Thinking broadly, our process for generating mnemonic haiku will be very similar to the process we used to mine for Bitcoin vanity addresses. We’ll repeatedly generate mnemonic sequences, filtering out those that don’t satisfy the structural criteria of a haiku.
Let’s create a new module to hold this functionality:
defmodule Bip39Haiku do
end
Inside our new Bip39Haiku
module, we’ll create a new stream/0
function that returns an Elixir stream of valid mnemonic haiku, sketching out helper functions as we go:
def stream do
fn ->
Bip39Haiku.Mnemonic.generate()
end
|> Stream.repeatedly()
|> Stream.filter(&Bip39Haiku.Haiku.is_valid?/1)
end
We create an anonymous function that generates a BIP-39 mnemonic with a call to our previously implemented Bip39Haiku.Mnemonic.generate/0
. Next, we pass our anonymous function into Stream.repeatedly/1
to create an infinite stream of mnemonic sequences. Lastly, we use Stream.filter/2
to filter out mnemonic sequences that aren’t haiku.
All we have to do is implement Bip39Haiku.Haiku.is_valid?/1
!
Validating Haiku
Our Bip39Haiku.Haiku.is_valid?/1
function will return true
when given a wordlist
that satisfies our structural criteria, and false
in all other cases. Let’s start fleshing it out:
defmodule Bip39Haiku.Haiku do
def is_valid?(wordlist) do
end
end
The first thing we need to do is associate each word in our wordlist
with its number of syllables. We’ll do this with a call to a attach_syllables/1
helper function:
wordlist = attach_syllables(wordlist)
For now we’ll wave our hands over the implementation of attach_syllables/1
, but let’s assume the the result is a list of tuples:
[{"cat", 1}, {"tomato", 3}, {"pizza", 2}]
The first element of each tuple contains the original word in the wordlist, and the second element of the tuple is the number of syllables in that word.
Filtering Unknown Words
Sometimes, our attach_syllables/1
function might be given a word it doesn’t recognize. In that case, it’ll claim the word has zero syllables. Since we don’t know how many syllables are actually in the word, we can’t use it to construct a haiku. In that case we’ll have to assume the entire wordlist is invalid.
with nil <- Enum.find(wordlist, &zero_syllables?/1) do
...
else
_ -> false
end
We can use Elixir’s with
special form to model our happy path. If we can’t find any words with zero syllables, we’re in the clear and are free to move on with our structural checks. Otherwise, we’ll return false
.
The zero_syllables?/1
function is a simple helper to find words with 0
syllables:
defp zero_syllables?({_, 0}), do: true
defp zero_syllables?({_, _}), do: false
Dropping Syllables
In general, our plan of attack for validating that a given wordlist
is a haiku is to attempt to remove each line of syllables, one after the other. If we can successfully remove each line of syllables, and there are no words left in the wordlist, we have a haiku. If anything goes wrong along the way, we don’t have a haiku.
We can update our happy path to express this strategy:
with nil <- Enum.find(wordlist, &zero_syllables?/1),
{:ok, wordlist} <- drop_syllables(wordlist, 5),
{:ok, wordlist} <- drop_syllables(wordlist, 7),
{:ok, wordlist} <- drop_syllables(wordlist, 5) do
Enum.empty?(wordlist)
else
_ -> false
end
Each call to drop_syllables/2
accepts a wordlist
, removes the specified number of syllables from the wordlist
(if possible), and returns the truncated list as a result.
The first step in writing drop_syllables/2
is to walk through our word list, accumulating the total number of syllables we’ve seen up until that point in the wordlist
. We can do this with a little help from Elixir’s Enum.scan/3
:
total_syllables =
wordlist
|> Enum.scan(0, fn {word, syllables}, total ->
total + syllables
end)
The total_syllables
list for our previous example of ["cat", "tomato", "pizza"]
would look like this:
[1, 4, 6]
Now all we have to do is find the index of our total_syllables
list whose value, the total number of syllables seen up until that word, matches the number of syllables we’re looking to remove from wordlist
:
index =
total_syllables
|> Enum.find_index(&(&1 == syllables))
If we can’t find an index
, that means it’s not possible to split off the desired number of syllables from the beginning of our wordlist
. In that case we return an error tuple:
case index do
nil ->
{:error, :not_possible}
index ->
{:ok, Enum.drop(wordlist, index + 1)}
end
Otherwise, we return our truncated wordlist
, dropping every word through our found index
.
Attaching Syllables
Let’s turn our attention to the previously glossed over attach_syllables/1
function.
Counting the number of syllables in a word is easy for us humans, but much more difficult for computers. To help out our computing allies, we’ll be using the Wordnik API to fetch the syllables in a given word.
Our attach_syllables/1
helper function will asynchronously look up the syllables in each of the words in wordlist
with a call to a new Bip39Haiku.Wordnik.get_syllables/1
function:
defp attach_syllables(wordlist) do
wordlist
|> Enum.map(
&Task.async(fn ->
{&1, Bip39Haiku.Wordnik.get_syllables(&1)}
end)
)
|> Enum.map(&Task.await(&1, :infinity))
end
Once we’ve fetched the syllables from Wordnik, attach_syllables/1
will pair the syllables with their associated words in a tuple, just as we expect.
Fetching Syllables
Because this is a mad science experiment and not a serious software development project, we’ll keep our get_syllables/1
function as simple as possible. To reiterate, get_syllables/1
should accept a word
and return the number of syllables in that word:
defmodule Bip39Haiku.Wordnik do
def get_syllables(word) do
end
end
The first thing we need to do to fetch syllable information from Wordnik is to load our Wordnik API key:
wordnik_api_key =
Application.fetch_env!(
:bip39_haiku,
:wordnik_api_key
)
In our application’s configuration, we’ll pull the API key from the system’s environment variables, falling back to Wordnik’s demo API key if one isn’t available:
config :bip39_haiku,
wordnik_api_key:
System.get_env("WORDNIK_API_KEY") ||
"a2a73e7b926c924fad7001ca3111acd55af2ffabf50eb4ae5"
Next, we’ll build the URL for the API endpoint we want to use. In this case, we’ll be using Wordnik’s hyphenation endpoint:
endpoint =
"http://api.wordnik.com/v4/word.json/#{word}/hyphenation?api_key=#{
wordnik_api_key
}"
Lastly, we’ll make our request. If the request is successful, we’ll parse the HTTPotion response object with a call to our parse_response/1
helper. Otherwise, we’ll naively try to fetch the syllables again with a recursive call to get_syllables/1
:
case HTTPotion.get(endpoint) do
%HTTPotion.Response{
status_code: 200,
body: body
} ->
parse_response(body)
_ ->
get_syllables(word)
end
Our parse_response/1
function decodes the resulting JSON string returned by the API and counts the number of unique syllables in the provided word:
defp parse_response(body) do
body
|> Poison.decode!()
|> Enum.map(& &1["seq"])
|> Enum.uniq()
|> length
end
That’s it!
Now that we’ve finished all of the components of our mnemonic haiku miner, let’s put it to the test!
On Being a Respectful API Consumer
It’s worth noting that in its current incarnation, our get_syllables/1
function is incredibly inefficient. It will hit the Wordnik API for every word
passed into it, even if its seen that word before. This isn’t very respectful, and will surely result in our application running up against the API’s rate limits.
An immediate and significant optimization for this haiku miner would be to add a database layer that stores each word along with its syllable count after receiving each result from the Wordnik API. Subsequent calls to get_syllables/1
could avoid calls to the Wordnik API by returning cached results from the database.
That said, this article is already too long, and the value of a BIP-39 mnemonic haiku generator is questionable at best, so I’ll leave this improvement as an exercise for the reader.
A Warning - Don’t Use These Seeds!
Before I end this article, I feel that it’s important to mention in big, bold letters that the mnemonics generated with this tool should not be used to manage real Bitcoin wallets.
By restricting your wallet’s seed entropy to just the subset of random bytes that result in the generation of a structurally sound haiku, you’re drastically reducing the practical security of your wallet.
This project is only intended to be an experiment and a learning exercise.
Final Thoughts
Now that our haiku miner is finished, we can revel in the beauty of our cryptographically secure, randomly generated poetry.
useless squeeze topic
blind lawsuit quit tube hamster
reason empower
Beauty is in the eye of the beholder, I guess.
This was definitely a weird tangent, but this idea has been rolling around in the back of my head for weeks now. Now that I’ve built my mnemonic haiku miner, maybe I’ll find some peace. Be sure to check out the whole project on Github.
If you find this kind of Bitcoin development interesting, I highly recommend you check out Andreas Antonopoulos’ Mastering Bitcoin. Most of the examples in the book are written in Python and C, but as my previous Master Bitcoin articles demonstrate, Bitcoin development is perfectly suited for Elixir.
If you have any other ideas for mad science experiments with anything Elixir or Bitcoin related, let me know on Twitter!