¿Cómo hace Electrum un par de claves a partir de una semilla?

Electrum utiliza una semilla de 12 palabras para generar un par de claves y luego genera jerárquicamente direcciones a partir de ese par de claves.

Sé cómo generar direcciones jerárquicas a partir de un par de claves, pero lo que no entiendo es cómo Electrum genera un par de claves a partir de la semilla. Entiendo que la semilla consiste en palabras que provienen de una lista de palabras de 2048 de longitud, pero después de eso no me queda claro cómo comienza a generar la secuencia de direcciones.


Me gustaría hacer la generación HD en Java/C++/Python o C#. Si puedo entender el proceso, podría reproducirlo yo mismo y generar direcciones jerárquicas a partir de una semilla.


EDITAR:

Un día después encuentro esta implementación de Java... ¡Gracias por las respuestas!

https://github.com/harningt/atomun-mnemónico

Respuestas (1)

Las billeteras creadas por Electrum 1.x tienen semillas que contienen 12 palabras (24 palabras también son posibles para semillas creadas a medida ). Dada una matriz de base cero de seed_words de esa longitud, este pseudocódigo calcula master_private_key:

i = 0
while i < length(seed_words):
    # convert each word into an int in the range [0,1625]
    # based on the word's position in the sorted word list
    seed_ints[i] = lookup_seed_word(seed_words[i])
    i = i + 1

num_words  = 1626
num_words2 = num_words * num_words
seed_hex_str = ""
i = 0
while i < length(seed_words):
    # (hex8 converts an int into an ASCII string of
    # exactly 8 zero-padded lowercase hex digits;
    # % is the integer remainder operator
    seed_hex_str = seed_hex_str + hex8( seed_ints[i    ]
                    + num_words  * (   (seed_ints[i + 1] - seed_ints[i    ]) % num_words )
                    + num_words2 * (   (seed_ints[i + 2] - seed_ints[i + 1]) % num_words ))
    i = i + 3

unstretched_seed = ascii_string_to_byte_array(seed_hex_str)
seed = byte_array()  # an empty byte array
i = 0
while i < 100000:
    # sha256 operates on and produces byte arrays
    seed = sha256(seed + unstretched_seed)
    i = i + 1

master_private_key = byte_array_to_int(seed, order=big_endian)

En caso de que se lo pregunte, parece que la razón por la que el cálculo de seed_ints parece demasiado complejo puede ser para evitar infringir una patente .

A modo de comparación, las billeteras creadas por Electrum 2.x generalmente tienen semillas que contienen 13 palabras, sin embargo, en ocasiones tendrán menos. (Es técnicamente posible construir semillas de casi cualquier longitud que se aceptarán al restaurar una billetera Electrum 2.x). Aquí está el pseudocódigo que calcula una clave privada maestra extendida BIP-32:

# Electrum 2.x doesn't separate mnemonic words with spaces in sentences for any CJK
# scripts when calculating the checksum or deriving a binary seed (even though this
# seems inappropriate for some CJK scripts such as Hiragana as used by the ja wordlist)
if language is CJK:
    space = ""
else:
    space = " "

seed_phrase = ""
i = 0
do:
    word = seed_words[i]
    normalize_unicode(word, normalization=nfkd)
    remove_unicode_combining_marks(word)  # e.g. accent marks
    seed_phrase = seed_phrase + word
    i = i + 1
    if i ≥ length(seed_words):
        exit-loop
    seed_phrase = seed_phrase + space

seed_utf8 = unicode_to_byte_array(seed_phrase, format=utf8)

if hmac_sha512(key="Seed version", message=seed_utf8)[0] ≠ 1:
    fail("invalid checksum")

stretched_seed = pbkdf2_hmac_sha512(password=seed_utf8, salt="electrum", iterations=2048, output_length=64)
seed_bytes = hmac_sha512(key="Bitcoin seed", message=stretched_seed)

private_key        = byte_array_to_int(seed_bytes[0..31], order=big_endian)
chain_code_bytes   = seed_bytes[32..63]
master_private_key = create_bip32_extended_private_key(private_key, chain_code_bytes)

BIP-39 , una técnica de derivación alternativa, es similar a Electrum 2.x pero no idéntica.

"seed = sha256(seed + unstretched_seed)" o seed = sha256(seed) + sha256(unstretched_seed))" No estoy al tanto de un método SHA256 que toma matrices de dos bytes.
Tienes razón, no lo hace. El '+' estaba destinado a transmitir concatenación. IOW, el unstretched_seed original se agrega al resultado del último sha256 antes de que se tome su hash en cada iteración. Para ver un ejemplo más concreto, consulte aquí: github.com/gurnec/btcrecover/blob/…
@MrJones FYI Arreglé un error en el pseudocódigo de Electrum 1.x: num_words estaba mal. Lo lamento.
Confirmo lo de la evasión de patentes.