Claves privadas de Bitcoin ¿Qué son en realidad? [Bitcoin para programadores]
¿Qué es una clave privada de Bitcoin?
Muchos de nosotros hemos trasteado alguna vez con las llaves privadas de nuestras direcciones de Bitcoin, ya sea generando una billetera en papel ("Paper wallet") o almacenándolas en dispositivos lejos de una conexión a Internet ("Cold storage") pero, ¿Alguna vez te has preguntado cómo se generan ésas llaves privadas? ¿Qué contienen esas series de letras aparentemente sin sentido? La verdad es que no es nada del otro mundo. Una llave privada de Bitcoin es un número secreto que permite a un usuario gastar los Bitcoins correspondientes a su dirección, las claves privadas estan íntimamente ligadas a su dirección de Bitcoin correspondiente. Muchas personas tienden a confundir el término "billetera" (wallet) con software especializado o con las direcciones de Bitcoin (address), pero son cosas distintas. Mi definición personal de una billetera es la siguiente:
Una billetera es un archivo o dispositivo especial cuya función primordial es la de almacenar claves privadas.
Tan simple como eso.
Dicho archivo puede presentarse en distintos formatos, un simple archivo de texto plano que contenga llaves privadas de Bitcoin es en sí una billetera; hay billeteras encriptadas o con formatos especiales que sólo un software especializado puede leer (como electrum, JAX, Bitcoin core, Exodus, etc.).
Ahora que tenemos claro que existe SOFTWARE(programas) que gestiona las LLAVES PRIVADAS almacenadas en una BILLETERA (archivo o hard wallet), procedamos a definir qué es en realidad una llave privada.
Una llave privada de Bitcoin se presenta generalmente como un número de 256 bits (aunque algunas de las nuevas pueden presentarse entre 128 y 512) generado aleatoriamente por un software especializado , que puede ser representado de diferentes maneras (binario, decimal, hexadecimal, base58, etc), éste concepto se entenderá mejor con un ejemplo práctico.
Generando una clave privada
Para obtener una llave privada basta con generar un número aleatorio que quepa en 256 bits, es así de simple, no hay truco en ello.
La manera en la que se puede generar ése número depende tanto del programador como del lenguaje, pero algunas aproximaciones funcionales podrían ser las siguientes:
1.- Generando el número completo aleatoriamente.
2.- Generando 32 números aleatorios de 1 byte y almacenándolos en un arreglo, que en conjunto conformarán la clave privada
3.- Usando alguno de los métodos anteriores, pero agregando además entropía de algún tipo (posición del mouse, latencia del disco duro, ruido en señales, etc). Éste método puede verse claramente en códigos que generan las paper wallets con javascript, por ejemplo.
4.- Generando 64 números aleatorios de 4 bits (entre 0 y 15) cada uno con ayuda de 3 dados (Sí, dados comunes y corrientes)
Tomemos como ejemplo el siguiente código en python (que puede traducirse fácilmente a otros lenguajes):
def getPrivateKeyBytes():
# 32 bytes (256 bits) random stream:
return os.urandom(32);
La función llamada getPrivateKeyBytes() devuelve una secuencia de 32 bytes cuyos valores se generan aleatoriamente mediante la función os.urandom(). Dicha secuencia de bytes puede ser representada de varias maneras. Por ejemplo, aquí te presento una llave privada representada de 3 maneras diferentes, siendo la tercera la más popular puesto que es la representación usada por la mayoría de los programas a la hora de exportar las llaves privadas:
Decimal:
100563969971778840833093929713430626829600307103210223668292258499064871955324
Hexadecimal:
DE55309F9194816A32CDA083FCE44CB2B53E9B505F7B5338C6D101F6CF06AB7C
WIF (Wallet Import Format):
5KWCmqJwgwuHbSjYrH4cXqmGJFV8k73Lju4Y91GoevB6SDUF9W1
Las 3 representaciones corresponden a la misma llave privada, sólo que en distintos formatos. A fin de cuentas una llave privada no es otra cosa que un número enorme el cual es extremadamente difícil adivinar por casualidad.
Formato de claves privadas (Hexadecimal y WIF)
Volviendo al ejemplo anterior, una vez que tenemos un número, lo siguiente es obtener la representación requerida, las 2 más comunes son la Hexadecimal y la WIF.
Para obtener la representación hexadecimal, basta con crear una función que reciba nuestra secuencia de bytes y las transforme:
def getHexPrivateKey(privateKeyBytes):
# Hexadecimal representation:
hexKey = privateKeyBytes.encode('hex')
# Hexadecimal key to uppercase:
return hexKey.upper()
La función recibe los bytes (privateKeyBytes
) y los codifica en hexadecimal con la función encode()
, el resultado es una cadena que contiene la representación hexadecimal de la clave privada en minúsculas (hexKey
), por eso se usa la función upper()
para transformar la cadena a mayúsculas. Éste último paso no es necesario y no afecta en lo absoluto el proceso futuro de la llave, pero las claves se ven mejor en mayúsculas.
¿Y qué hay de las claves WIF?
Las claves en Wallet Import Format (WIF) se generan mediante el algoritmo descrito en:
https://en.bitcoin.it/wiki/Wallet_import_format
Éste tipo de formato se caracteriza por tener un número 5 al principio y en éste caso sí importan las mayúsculas y las minúsculas por razones que quedarán más claras a continuación.
Para seguir los pasos del algoritmo hay que entender primero la codificación base58 para Bitcoin:
https://en.bitcoin.it/wiki/Base58Check_encoding
Para entender de qué se trata podemos tomar un ejemplo simple de codificación en hexadecimal:
Sabemos que el "alfabeto" del código hexadecimal es el siguiete: "0123456789ABCDEF"
En dicho código el valor más alto es el 15 (F) y después del número 9, los valores se representan con las letras en el rango A:F.
Por lo que los números decimales se representan como sigue:
Decimal Hexadecimal
8 8
9 9
10 A
11 B
12 C
13 D
Y así sucesivamente.
Bueno, con la codificación base58 ocurre exactamente lo mismo, sólo que el alfabeto consta de los siguientes valores:
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
Donde el valor máximo (57) corresponde a la letra 'z'. Por lo que:
Decimal Hexadecimal Base58
8 8 9
9 9 A
10 A B
11 B C
12 C D
13 D E
Y así sucesivamente.
¿Para qué sirve ésta codificación? Bueno, si eres un poco observador, ya habrás notado que el alfabeto de base58 no cotiente algunos elementos. Esto es por que la codificación base58 es usada principalmente para representar claves WIF y direcciones de Bitcoin, los únicos caracteres prohibidos son la letra 'O' mayúscula, el número '0', la letra mayúscula 'I' y la letra minúscula 'l' con el fin de evitar ambigüedad visual. Los dos primeros podrían confundirse entre sí y con la letra 'o' minúscula, mientras que los dos segundos podrían confundirse entre sí y con el número '1'.
En python, una función que codifique en base58 podría ser la siguiente:
def base58Encode(value):
# Base58 character range:
alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
# Contains the final result:
encoded = ''
# Encoding:
num = value
while num >= 58:
mod = num % 58
encoded = alphabet[mod] + encoded
num = (num - mod) / 58
if num >gt; 0 :
encoded = alphabet[num] + encoded;
return encoded;
Donde el parámetro value
sería un número entero (una clave privada o una dirección de Bitcoin).
Ahora que comprendes de que va más o menos la codificación base58, es momento de volver al algoritmo para generar claves WIF.
def getWIFPrivateKey(privateKeyBytes): # 1.- Se toman los bytes de la llave privada (privateKeyBytes)
# 2.- Se agrega un byte 0x80 al principio para mainnet (la red principal) o 0xef para testnet (red de pruebas).
# Adicionalmente se puede agregar un byte 0x01 al principio si la llave privada corresponderá
# a una llave pública comprimida (por ejemplo, para las direcciones de Segwit):
extendedKey = '\x80'+privateKeyBytes
# 3.- Se obtiene el hash resultante de aplicar SHA256 sobre la llave extendida:
extendedKeyHash = hashlib.sha256(extendedKey)
# 4.- Se obtiene el hash resultante de aplicar SHA256 sobre el hash anterior:
extendedKeyHash = hashlib.sha256( extendedKeyHash.digest() )
# 5.- Se toman los primeros 4 bytes (32 bits) del segundo hash SHA-256 éstos bytes son el checksum:
checksum = extendedKeyHash.digest()[:4]
# 6.- Se añaden los 4 bytes del checksum al final de la clave extendida del paso 2:
extendedKey += checksum
#7.- Se codifica el resultado en base58
# El resultado final es la clave WIF:
extendedKeyInt = int(extendedKey.encode('hex'), 16)
WIFKey = base58.base58Encode(extendedKeyInt)
return WIFKey
En futuras publicaciones mostraré los algoritmos para generar la dirección de Bitcoin asociada a una clave privada. El método es más complejo, por ahora resumiré el código de ejemplo aquí:
base58.py
# Coded by Reshawn Sullivan Sep-04-2017
# Base58 encoding, as described at:
# https://en.bitcoin.it/wiki/Base58Check_encoding
# @param value Value (integer) to be encoded
def base58Encode(value):
# Base58 character range:
alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
# Contains the final result:
encoded = ''
# Encoding:
num = value
while num >= 58:
mod = num % 58
encoded = alphabet[mod] + encoded
num = (num - mod) / 58
if num >gt; 0 :
encoded = alphabet[num] + encoded;
return encoded;
BitcoinKeys.py
# Coded by Reshawn Sullivan Sep-04-2017
# Functions to generate random Bitcoin private keys
import os
import hashlib
import base58
# Returns a randomly generated stream of 32 bytes (a 256 bits random number)
# that represents a 256 bits standard Bitcoin private key.
def getPrivateKeyBytes():
# 32 bytes (256 bits) random stream:
return os.urandom(32);
# Returns the hexadecimal representation of a Bitcoin private key.
# @param privateKeyBytes A 256 bits stream (private key)
def getHexPrivateKey(privateKeyBytes):
# Hexadecimal representation:
hexKey = privateKeyBytes.encode('hex')
# Hexadecimal key to uppercase:
return hexKey.upper()
# Gets the WIF (Wallet import format) of a Bitcoin private key,
# bassed on https://en.bitcoin.it/wiki/Wallet_import_format docu.
# @param privateKeyBytes A 256 bits stream (private key)
def getWIFPrivateKey(privateKeyBytes): # 1.- Take a private key
# 2.- Add a 0x80 byte in front of it for mainnet addresses or 0xef for testnet addresses.
# Also add a 0x01 byte at the end if the private key will correspond to a compressed public key:
extendedKey = '\x80'+privateKeyBytes
# 3.- Perform SHA-256 hash on the extended key:
extendedKeyHash = hashlib.sha256(extendedKey)
# 4.- Perform SHA-256 hash on result of SHA-256 hash:
extendedKeyHash = hashlib.sha256( extendedKeyHash.digest() )
# 5.- Take the first 4 bytes of the second SHA-256 hash, this is the checksum
checksum = extendedKeyHash.digest()[:4]
# 6.- Add the 4 checksum bytes from point 5 at the end of the extended key from point 2
extendedKey += checksum
# 7.- Convert the result from a byte stream into a base58 string using Base58Check encoding.
# This is the Wallet Import Format
extendedKeyInt = int(extendedKey.encode('hex'), 16)
WIFKey = base58.base58Encode(extendedKeyInt)
return WIFKey
wallet.py
# Coded by Reshawn Sullivan Sep-04-2017
import BitcoinKeys
# 256 randomly generated bits stream (a Bitcoin private key):
privateKey = BitcoinKeys.getPrivateKeyBytes()
# Hexadecimal representation of a Bitcoin private key:
hexPrivateKey = BitcoinKeys.getHexPrivateKey(privateKey)
# WIF (Wallet import format) of a Bitcoin private key:
WIFPrivateKey = BitcoinKeys.getWIFPrivateKey(privateKey)
#print private key:
print 'Private Key (hex): '+hexPrivateKey
#print WIF:
print 'Private Key (WIF): '+WIFPrivateKey+'\n'
No te olvides de poner el tag spanish en tus posts.
Si tienes dudas acerca del funcionamiento te invito a que leas las Steemit FAQ:
Steemit FAQ #1
Steemit FAQ #2
Hemos creado tambien un chat en discord donde interactuamos los unos con los otros y nos promocionamos.
Y no te pierdas los audioconferencias entre los miembros cervantiles y los canales de promocion por categorias
Tambien estamos en Steemitchat https://steemit.chat/channel/HispanoHablantes
valorado en 2000 SBD!!!