miércoles, 24 de noviembre de 2021

Minar Bitcoin con Java desde cero: el protocolo crudo paso a paso

Bitcoin · Java · Protocolo crudo

Minar Bitcoin con Java desde cero

Sin APIs, sin librerías externas y sin el código fuente de Bitcoin. Solo Java puro, SHA-256 y muchos bytes.

⏱️ Unos 25 minutos de lectura · Nivel intermedio · Código listo para copiar · Actualizado en noviembre de 2026

01. Por qué escribir un minero en Java

Soy Daniel, venezolano y aficionado a la programación. Programo en varios lenguajes, pero mi favorito es Java. Desarrollo de forma independiente, por pura pasión, y hoy quiero compartir lo que he aprendido sobre criptografía y criptomonedas: en concreto, cómo funciona Bitcoin por dentro.

Voy a explicar y escribir un minero de Bitcoin en Java puro, sin APIs, sin librerías externas y sin partir del código fuente oficial. Solo el protocolo crudo de Bitcoin, implementado paso a paso. El programa sirve tanto para minar un bloque nuevo como para verificar uno que ya fue extraído.

💡 Aviso honesto: esto es un tutorial educativo. Con Java y una computadora normal no vas a competir con granjas de ASIC; la idea es entender el protocolo, no hacerte rico minando. Dicho eso, el código funciona de verdad.

Java es razonablemente rápido, aunque corre sobre una máquina virtual. Para minar en serio se usan C o C++, que hablan más directamente con el hardware. Eso no quita que sea perfectamente posible implementar el protocolo y aprender con Java.

Lo que vas a aprender

  • Cómo se representa un byte y por qué Java lo complica un poco.
  • Qué son big-endian y little-endian, y cuándo se usa cada uno.
  • Qué campos forman la cabecera de un bloque y cuánto mide cada uno.
  • Cómo se hace el doble SHA-256 y cómo se compara con el objetivo de dificultad.
  • Cómo se calculan el Merkle root y el timestamp.
  • Todo el código Java, método por método, listo para copiar.

02. El byte: la unidad con la que piensa Bitcoin

En informática, la unidad básica de información es el byte. Un byte son 8 bits: una combinación de ceros y unos de longitud 8. Con 8 bits hay 256 combinaciones posibles, así que un byte guarda valores de 0 a 255.

Por ejemplo, el byte 01011101 se escribe así en distintos sistemas:

BinarioDecimalHexadecimalOctal
01011101935D135
11111111255FF377

Aquí aparece la primera trampa: en Java no existen los enteros sin signo. Un byte de Java va de -128 a 127, no de 0 a 255. El valor 255 se guarda como -1, y así sucesivamente.

⚠️ Ojo: si no tienes esto en cuenta, el hash te va a salir mal y no vas a saber por qué. Cuando necesites el valor sin signo, usa byte & 0xFF o trabaja con int.

Esta diferencia es justo la que obliga a hacer conversiones cuidadosas al implementar el protocolo Bitcoin en Java.

03. Big-endian y little-endian sin dolor

Cuando escribimos un número, ponemos el dígito más significativo a la izquierda. Ese orden se llama big-endian y es el que ves en los exploradores de bloques: los hashes y las transacciones se muestran así porque es más cómodo para las personas.

Bitcoin, en cambio, trabaja internamente en little-endian: el byte menos significativo va primero, es decir, los bytes se invierten. Un detalle clave: cada byte son dos dígitos hexadecimales, así que un carácter hexadecimal es medio byte.

ValorBig-endianLittle-endian
1 byte4D4D
2 bytesC78E8EC7

Con un solo byte no cambia nada. Con dos o más, se invierte el orden de los bytes, no los dígitos de cada byte.

📌 Regla de oro: lo que muestras en pantalla va en big-endian; lo que se concatena y se hashea va en little-endian. Si el hash no coincide, revisa primero el endianness.

04. Qué se mina: la cabecera de 80 bytes

Minar un bloque no es hashear las transacciones directamente. Lo que se mina es la cabecera del bloque: 80 bytes (640 bits) con seis campos. Los demás datos del bloque no entran en el hash.

CampoBytesQué es
Versión4Versión del protocolo, en hexadecimal little-endian.
Hash anterior32Hash del bloque previo, en little-endian.
Merkle root32Hash de todas las transacciones del bloque, incluida la CoinBase.
Timestamp4Segundos desde el 1 de enero de 1970, 00:00:00 UTC.
Bits4Dificultad codificada; de aquí sale el objetivo.
Nonce4Número que el minero cambia hasta encontrar un hash válido.

El Merkle root merece una mención aparte: resume todas las transacciones del bloque en un solo hash. Más adelante verás cómo se calcula.

05. Cómo funciona el minado

El proceso es más simple de lo que parece:

  1. Concatenas los seis campos de la cabecera en little-endian.
  2. Les aplicas doble SHA-256: SHA-256(SHA-256(cabecera)).
  3. Comparas el resultado con el objetivo que sale del campo Bits.
  4. Si no cumple, cambias el nonce y repites.

Un hash es válido si, leído como número, es menor que el objetivo. En la representación big-endian que muestran los exploradores, eso se ve como una cierta cantidad de ceros a la izquierda. Cuanto más pequeño es el objetivo, más difícil es encontrarlo y más energía hace falta.

✅ Dato: la Versión y los Bits vienen dados por el protocolo y por la red; el Timestamp y el Merkle root se calculan; el nonce es el que se mueve. Todo empieza a encajar cuando ves el hash correcto aparecer.

Con el bloque 709130 el programa genera este hash:

0000000000000000000aa425b68a7e425b01ec4fa966b42e330e13535b7ee947
Salida del programa minero con el bloque 709130
Salida del minero al verificar el bloque 709130.

06. Manos al código

Todo el código es Java estándar. No hace falta Maven ni dependencias: crea una clase, pega el método y listo. Puedes copiar cada bloque con el botón de copiar de tu navegador.

6.1 SHA-256 en Java

Java ya trae SHA-256 en java.security.MessageDigest. Este método recibe un texto y devuelve su hash en hexadecimal:

JavaSHA256.java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public String cifrarTexto(String texto) {
    MessageDigest codigo = null;
    try {
        codigo = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
        return "No existe";
    }
    byte[] hash = codigo.digest(texto.getBytes());
    StringBuilder cifrado = new StringBuilder();
    for (byte bit : hash) {
        cifrado.append(String.format("%02x", bit));
    }
    return cifrado.toString();
}

El método toma los bytes de la cadena, aplica SHA-256 y convierte el arreglo de bytes resultante a hexadecimal. Puedes ponerlo en cualquier clase y llamarlo donde lo necesites.

6.2 Convertir hexadecimal a decimal

Este método es didáctico: recorre cada carácter hexadecimal y lo transforma en su valor decimal. Es largo, pero deja ver el proceso. En producción podrías usar Integer.parseInt(texto, 16).

JavaHexADecimal.java
private int hexadecimalDecimal(String texto) {
    int[] numero = new int[texto.length()];
    for (int i = 0; i < texto.length(); i++) {
        if (texto.charAt(i) == '0') {
            numero[i] = 0;
        }
        if (texto.charAt(i) == '1') {
            numero[i] = 1;
        }
        if (texto.charAt(i) == '2') {
            numero[i] = 2;
        }
        if (texto.charAt(i) == '3') {
            numero[i] = 3;
        }
        if (texto.charAt(i) == '4') {
            numero[i] = 4;
        }
        if (texto.charAt(i) == '5') {
            numero[i] = 5;
        }
        if (texto.charAt(i) == '6') {
            numero[i] = 6;
        }
        if (texto.charAt(i) == '7') {
            numero[i] = 7;
        }
        if (texto.charAt(i) == '8') {
            numero[i] = 8;
        }
        if (texto.charAt(i) == '9') {
            numero[i] = 9;
        }
        if (texto.charAt(i) == 'a') {
            numero[i] = 10;
        }
        if (texto.charAt(i) == 'b') {
            numero[i] = 11;
        }
        if (texto.charAt(i) == 'c') {
            numero[i] = 12;
        }
        if (texto.charAt(i) == 'd') {
            numero[i] = 13;
        }
        if (texto.charAt(i) == 'e') {
            numero[i] = 14;
        }
        if (texto.charAt(i) == 'f') {
            numero[i] = 15;
        }
        if (texto.charAt(i) == 'A') {
            numero[i] = 10;
        }
        if (texto.charAt(i) == 'B') {
            numero[i] = 11;
        }
        if (texto.charAt(i) == 'C') {
            numero[i] = 12;
        }
        if (texto.charAt(i) == 'D') {
            numero[i] = 13;
        }
        if (texto.charAt(i) == 'E') {
            numero[i] = 14;
        }
        if (texto.charAt(i) == 'F') {
            numero[i] = 15;
        }
    }
    int numeroDecimal = numero[1] + numero[0] * (16);
    return numeroDecimal;
}

6.3 Hexadecimal a bytes: los dos sentidos

Para hashear necesitas bytes, no texto. El primer método convierte una cadena hexadecimal a un arreglo de bytes en big-endian:

JavaBytes.java
public byte[] bytesNoInvertidos(String texto) {
    byte[] pares = new byte[texto.length() / 2];
    if (!(texto == "" || texto == " " || texto == null)) {
        StringBuilder[] paresTexto = new StringBuilder[texto.length()/ 2];
        for (int i = 0; i < paresTexto.length; i++) {
            paresTexto[i] = new StringBuilder();
        }
        int j = 0;
        for (int i = 0; i < paresTexto.length; i++) {
            paresTexto[i].append(texto.charAt(j));
            j++;
            paresTexto[i].append(texto.charAt(j));
            j++;
        }
        for (int i = 0; i < paresTexto.length; i++) {
            pares[i] = (byte) hexadecimalDecimal(paresTexto[i].toString());
        }
    }
    return pares;
}

El segundo hace lo mismo, pero invierte los bytes para trabajar en little-endian, que es como los necesita el protocolo:

JavaBytes.java
public byte[] bytesInvertidos(String texto) {
    byte[] paresInvertidos = new byte[texto.length() / 2];
    StringBuilder[] paresTexto = new StringBuilder[texto.length() / 2];
    for (int i = 0; i < paresTexto.length; i++) {
        paresTexto[i] = new StringBuilder();
    }
    int j = 0;
    for (int i = 0; i < paresTexto.length; i++) {
        paresTexto[i].append(texto.charAt(j));
        j++;
        paresTexto[i].append(texto.charAt(j));
        j++;
    }
    byte[] pares = new byte[texto.length() / 2];
    for (int i = 0; i < paresTexto.length; i++) {
        pares[i] = (byte) hexadecimalDecimal(paresTexto[i].toString());
    }
    int indice = 0;
    for (int i = pares.length - 1; i >= 0; i--) {
        paresInvertidos[indice] = pares[i];
        indice++;
    }
    return paresInvertidos;
}

📌 Nota: en la entrada original este método aparecía duplicado. Aquí queda una sola vez, que es todo lo que necesitas.

6.4 El minero: doble SHA-256 de la cabecera

Este método arma la cabecera con los seis campos, le aplica doble SHA-256 y devuelve el hash en big-endian:

JavaMinero.java
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public String cifrarTextoManualHex(String versionHexadecimal, String hashAnterirHexadecimal,
        String MerkleRootHexadecimal, String tiempoHexadecimal, String tamanoBytesHexadecimal,
        String nonceHexadecimal) {
    byte[] cabecera = new byte[versionHexadecimal.length() / 2 + hashAnterirHexadecimal.length() / 2
            + MerkleRootHexadecimal.length() / 2 + tiempoHexadecimal.length() / 2
            + tamanoBytesHexadecimal.length() / 2 + nonceHexadecimal.length() / 2];
    byte[] version = new byte[versionHexadecimal.length() / 2];
    byte[] hashAnterior = new byte[hashAnterirHexadecimal.length() / 2];
    byte[] merkle = new byte[MerkleRootHexadecimal.length() / 2];
    byte[] tiempo = new byte[tiempoHexadecimal.length() / 2];
    byte[] tamanoBytes = new byte[tamanoBytesHexadecimal.length() / 2];
    byte[] nonce = new byte[nonceHexadecimal.length() / 2];
    version = binario.bytesInvertidos(versionHexadecimal);
    hashAnterior = binario.bytesInvertidos(hashAnterirHexadecimal);
    merkle = binario.bytesInvertidos(MerkleRootHexadecimal);
    tiempo = binario.bytesInvertidos(tiempoHexadecimal);
    tamanoBytes = binario.bytesInvertidos(tamanoBytesHexadecimal);
    nonce = binario.bytesInvertidos(nonceHexadecimal);
    int indice = 0;
    for (int i = 0; i < version.length; i++) {
        cabecera[indice] = version[i];
        indice++;
    }
    for (int i = 0; i < hashAnterior.length; i++) {
        cabecera[indice] = hashAnterior[i];
        indice++;
    }
    for (int i = 0; i < merkle.length; i++) {
        cabecera[indice] = merkle[i];
        indice++;
    }
    for (int i = 0; i < tiempo.length; i++) {
        cabecera[indice] = tiempo[i];
        indice++;
    }
    for (int i = 0; i < tamanoBytes.length; i++) {
        cabecera[indice] = tamanoBytes[i];
        indice++;
    }
    for (int i = 0; i < nonce.length; i++) {
        cabecera[indice] = nonce[i];
        indice++;
    }
    MessageDigest codigo = null;
    try {
        codigo = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
        return "No existe";
    }
    byte[] hash = codigo.digest(codigo.digest(cabecera));
    StringBuilder cifrado = new StringBuilder();
    for (byte bit : hash) {
        cifrado.append(String.format("%02x", bit));
    }
    StringBuilder[] datos = new StringBuilder[cifrado.toString().length() / 2];
    for (int i = 0; i < datos.length; i++) {
        datos[i] = new StringBuilder();
    }
    int in = 0;
    for (int i = 0; i < datos.length; i++) {
        datos[i].append(cifrado.toString().charAt(in));
        in++;
        datos[i].append(cifrado.toString().charAt(in));
        in++;
    }
    StringBuilder invertir = new StringBuilder();
    for (int i = datos.length - 1; i >= 0; i--) {
        invertir.append(datos[i].toString());
    }
    return invertir.toString();
}

Fíjate en el detalle: los campos entran en little-endian, el hash se calcula y al final se invierte para mostrarlo en big-endian, que es como lo verás en el explorador.

6.5 Validar contra el objetivo

El método anterior encuentra el hash, pero no comprueba si es válido. Esta versión sí lo hace: compara el hash con el objetivo y solo lo devuelve si es menor.

JavaMineroValido.java
public String cifrarTextoManualHexValido(String versionHexadecimal, String hashAnterirHexadecimal,
        String MerkleRootHexadecimal, String tiempoHexadecimal, String tamanoBytesHexadecimal) {
    StringBuilder inv = new StringBuilder();
    if (trabajo) {
        StringBuilder invertir = new StringBuilder();
        String nonceHexadecimal = binario.decimalHexadecimal(numeroDeTrabajo);
        numeroDeTrabajo++;
        byte[] cabecera = new byte[versionHexadecimal.length() / 2 + hashAnterirHexadecimal.length() / 2
                + MerkleRootHexadecimal.length() / 2 + tiempoHexadecimal.length() / 2
                + tamanoBytesHexadecimal.length() / 2 + nonceHexadecimal.length() / 2];
        byte[] version = new byte[versionHexadecimal.length() / 2];
        byte[] hashAnterior = new byte[hashAnterirHexadecimal.length() / 2];
        byte[] merkle = new byte[MerkleRootHexadecimal.length() / 2];
        byte[] tiempo = new byte[tiempoHexadecimal.length() / 2];
        byte[] tamanoBytes = new byte[tamanoBytesHexadecimal.length() / 2];
        byte[] nonce = new byte[nonceHexadecimal.length() / 2];
        version = binario.bytesInvertidos(versionHexadecimal);
        hashAnterior = binario.bytesInvertidos(hashAnterirHexadecimal);
        merkle = binario.bytesInvertidos(MerkleRootHexadecimal);
        tiempo = binario.bytesInvertidos(tiempoHexadecimal);
        tamanoBytes = binario.bytesInvertidos(tamanoBytesHexadecimal);
        nonce = binario.bytesInvertidos(nonceHexadecimal);
        int indice = 0;
        for (int i = 0; i < version.length; i++) {
            cabecera[indice] = version[i];
            indice++;
        }
        for (int i = 0; i < hashAnterior.length; i++) {
            cabecera[indice] = hashAnterior[i];
            indice++;
        }
        for (int i = 0; i < merkle.length; i++) {
            cabecera[indice] = merkle[i];
            indice++;
        }
        for (int i = 0; i < tiempo.length; i++) {
            cabecera[indice] = tiempo[i];
            indice++;
        }
        for (int i = 0; i < tamanoBytes.length; i++) {
            cabecera[indice] = tamanoBytes[i];
            indice++;
        }
        for (int i = 0; i < nonce.length; i++) {
            cabecera[indice] = nonce[i];
            indice++;
        }
        MessageDigest codigo = null;
        try {
            codigo = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            return "No existe";
        }
        byte[] hash = codigo.digest(codigo.digest(cabecera));
        StringBuilder cifrado = new StringBuilder();
        for (byte bit : hash) {
            cifrado.append(String.format("%02x", bit));
        }
        StringBuilder[] datos = new StringBuilder[cifrado.toString().length() / 2];
        for (int i = 0; i < datos.length; i++) {
            datos[i] = new StringBuilder();
        }
        int in = 0;
        for (int i = 0; i < datos.length; i++) {
            datos[i].append(cifrado.toString().charAt(in));
            in++;
            datos[i].append(cifrado.toString().charAt(in));
            in++;
        }
        for (int i = datos.length - 1; i >= 0; i--) {
            invertir.append(datos[i].toString());
        }
        String objetivo = binario.obtenerObjetivo(tamanoBytesHexadecimal);
        BigInteger nume1 = new BigInteger(binario.hexadecimalDecimalGigante(objetivo));
        BigInteger nume2 = new BigInteger(binario.hexadecimalDecimalGigante(invertir.toString()));
        if (nume2.compareTo(nume1) < 0) {
            trabajo = false;
            has = invertir;
        }
        inv = invertir;
    }
    if (trabajo) {
        return inv.toString();
    } else {
        return has.toString();
    }
}

La comparación se hace con BigInteger porque el número es demasiado grande para un long. Si el hash no cumple, el nonce se incrementa y se repite.

6.6 Del campo Bits al objetivo

El objetivo de dificultad se calcula a partir de los 4 bytes de Bits. Como el resultado puede tener 64 dígitos hexadecimales, se trabaja con BigInteger.

Primero, un conversor de hexadecimal gigante a decimal:

JavaHexadecimalGigante.java
public String hexadecimalDecimalGigante(String textoHexadecimal) {
    StringBuilder[] numeroDecimal = new StringBuilder[textoHexadecimal.length()];
    for (int i = 0; i < textoHexadecimal.length(); i++) {
        numeroDecimal[i] = new StringBuilder();
    }
    for (int i = 0; i < textoHexadecimal.length(); i++) {
        if (textoHexadecimal.charAt(i) == '0') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '1') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '2') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '3') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '4') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '5') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '6') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '7') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '8') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == '9') {
            numeroDecimal[i].append(textoHexadecimal.charAt(i));
        }
        if (textoHexadecimal.charAt(i) == 'a') {
            numeroDecimal[i].append("10");
        }
        if (textoHexadecimal.charAt(i) == 'b') {
            numeroDecimal[i].append("11");
        }
        if (textoHexadecimal.charAt(i) == 'c') {
            numeroDecimal[i].append("12");
        }
        if (textoHexadecimal.charAt(i) == 'd') {
            numeroDecimal[i].append("13");
        }
        if (textoHexadecimal.charAt(i) == 'e') {
            numeroDecimal[i].append("14");
        }
        if (textoHexadecimal.charAt(i) == 'f') {
            numeroDecimal[i].append("15");
        }
        if (textoHexadecimal.charAt(i) == 'A') {
            numeroDecimal[i].append("10");
        }
        if (textoHexadecimal.charAt(i) == 'B') {
            numeroDecimal[i].append("11");
        }
        if (textoHexadecimal.charAt(i) == 'C') {
            numeroDecimal[i].append("12");
        }
        if (textoHexadecimal.charAt(i) == 'D') {
            numeroDecimal[i].append("13");
        }
        if (textoHexadecimal.charAt(i) == 'E') {
            numeroDecimal[i].append("14");
        }
        if (textoHexadecimal.charAt(i) == 'F') {
            numeroDecimal[i].append("15");
        }
    }
    BigInteger[] numeros = new BigInteger[numeroDecimal.length];
    BigInteger[] partes = new BigInteger[numeroDecimal.length];
    BigInteger[] suma = new BigInteger[numeroDecimal.length - 1];
    BigInteger base = new BigInteger("16");
    for (int i = 0; i < numeros.length; i++) {
        numeros[i] = new BigInteger(numeroDecimal[i].toString());
    }
    int indice = numeros.length - 1;
    for (int i = 0; i < numeros.length; i++) {
        partes[i] = numeros[indice].multiply(base.pow(i));
        indice--;
    }
    suma[0] = partes[0].add(partes[1]);
    for (int i = 1; i < numeros.length - 1; i++) {
        suma[i] = suma[i - 1].add(partes[i + 1]);
    }
    BigInteger numeroDe = suma[suma.length - 1];
    return numeroDe.toString();
}

Luego, el cálculo del objetivo propiamente dicho:

JavaObjetivo.java
public String obtenerObjetivo(String bitHex) {
    int exponente = hexadecimalDecimal8(bitHex) >> 24;
    int numoroObtenido = hexadecimalDecimal8(bitHex) & 0xffffff;
    BigInteger numeroGrande = new BigInteger("1");
    int desplazar = (8 * (exponente - 3));
    BigInteger numeroGrande2 = new BigInteger(String.valueOf(numoroObtenido));
    BigInteger numeroGrande3 = numeroGrande.shiftLeft(desplazar);
    BigInteger numero = numeroGrande2.multiply(numeroGrande3);
    BigInteger base = new BigInteger("16");
    int potencia = 0;
    for (int i = 0; i < 1000; i++) {
        if (base.pow(i).compareTo(numero) >= 1) {
            potencia = i - 1;
            break;
        }
    }
    BigInteger[] division = new BigInteger[potencia];
    BigInteger[] resto = new BigInteger[potencia];
    division[0] = numero.divide(base.pow(potencia));
    resto[0] = numero.mod(base.pow(potencia));
    for (int i = 1; i < potencia; i++) {
        division[i] = resto[i - 1].divide(base.pow(potencia - i));
        resto[i] = resto[i - 1].mod(base.pow(potencia - i));
    }
    String[] num = new String[potencia + 1];
    for (int i = 0; i < potencia; i++) {
        num[i] = division[i].toString();
        if (i == potencia - 1) {
            num[i + 1] = resto[i].toString();
        }
    }
    StringBuilder numeroHex = new StringBuilder();
    for (int i = 0; i < num.length; i++) {
        for (int j = 0; j < 16; j++) {
            if (num[i].equals("" + j)) {
                if (j == 10) {
                    numeroHex.append("A");
                } else if (j == 11) {
                    numeroHex.append("B");
                } else if (j == 12) {
                    numeroHex.append("C");
                } else if (j == 13) {
                    numeroHex.append("D");
                } else if (j == 14) {
                    numeroHex.append("E");
                } else if (j == 15) {
                    numeroHex.append("F");
                } else {
                    numeroHex.append("" + j);
                }
            }
        }
    }
    String objetivo = hexadecimalAhexadecimal64(numeroHex.toString());
    return objetivo;
}

Estos dos métodos de apoyo completan el cálculo: uno interpreta 8 dígitos hexadecimales como entero, y el otro rellena con ceros a la izquierda hasta 64 caracteres.

JavaHex8.java
public int hexadecimalDecimal8(String texto) {
    int[] numero = new int[texto.length()];
    for (int i = 0; i < texto.length(); i++) {
        if (texto.charAt(i) == '0') {
            numero[i] = 0;
        }
        if (texto.charAt(i) == '1') {
            numero[i] = 1;
        }
        if (texto.charAt(i) == '2') {
            numero[i] = 2;
        }
        if (texto.charAt(i) == '3') {
            numero[i] = 3;
        }
        if (texto.charAt(i) == '4') {
            numero[i] = 4;
        }
        if (texto.charAt(i) == '5') {
            numero[i] = 5;
        }
        if (texto.charAt(i) == '6') {
            numero[i] = 6;
        }
        if (texto.charAt(i) == '7') {
            numero[i] = 7;
        }
        if (texto.charAt(i) == '8') {
            numero[i] = 8;
        }
        if (texto.charAt(i) == '9') {
            numero[i] = 9;
        }
        if (texto.charAt(i) == 'a') {
            numero[i] = 10;
        }
        if (texto.charAt(i) == 'b') {
            numero[i] = 11;
        }
        if (texto.charAt(i) == 'c') {
            numero[i] = 12;
        }
        if (texto.charAt(i) == 'd') {
            numero[i] = 13;
        }
        if (texto.charAt(i) == 'e') {
            numero[i] = 14;
        }
        if (texto.charAt(i) == 'f') {
            numero[i] = 15;
        }
        if (texto.charAt(i) == 'A') {
            numero[i] = 10;
        }
        if (texto.charAt(i) == 'B') {
            numero[i] = 11;
        }
        if (texto.charAt(i) == 'C') {
            numero[i] = 12;
        }
        if (texto.charAt(i) == 'D') {
            numero[i] = 13;
        }
        if (texto.charAt(i) == 'E') {
            numero[i] = 14;
        }
        if (texto.charAt(i) == 'F') {
            numero[i] = 15;
        }
    }
    int numeroDecimal = (int) (numero[7] + numero[6] * (16) + numero[5] * Math.pow(16, 2)
            + numero[4] * Math.pow(16, 3) + numero[3] * Math.pow(16, 4) + numero[2] * Math.pow(16, 5)
            + numero[1] * Math.pow(16, 6) + numero[0] * Math.pow(16, 7));
    return numeroDecimal;
}
JavaHex64.java
public String hexadecimalAhexadecimal64(String texto) {
    String t = texto;
    String tHex = "";
    StringBuilder ceros = new StringBuilder();
    for (int i = 0; i < 64; i++) {
        if (t.length() == i + 1) {
            for (int j = 0; j < 64 - (i + 1); j++) {
                ceros.append("0");
            }
            tHex = ceros + t;
        }
    }
    return tHex;
}

6.7 Calcular el Merkle root

El Merkle root resume todas las transacciones del bloque, incluida la CoinBase. Se van emparejando hashes y hasheando pares hasta que queda uno solo:

JavaMerkle.java
public String obtenerMerkle(String[] textos) {
    if (textos.length == 1) {
        return textos[0];
    }
    ArrayList<String> textosHashLista = new ArrayList<String>();
    for (int i = 0; i < textos.length - 1; i += 2) {
        textosHashLista.add(cifrarTransiciones(textos[i], textos[i + 1]));
    }
    if (textos.length % 2 == 1) {
        textosHashLista.add(cifrarTransiciones(textos[textos.length - 1], textos[textos.length - 1]));
    }
    String[] textosHash = new String[textosHashLista.size()];
    for (int i = 0; i < textosHash.length; i++) {
        textosHash[i] = textosHashLista.get(i);
    }
    return obtenerMerkle(textosHash);
}

Si el número de hashes es impar, el último se repite. Y cada par se hashea así:

JavaTxHash.java
public String cifrarTransiciones(String texto1, String texto2) {
    MessageDigest codigo = null;
    byte[] cabecera = new byte[texto1.length() / 2 + texto2.length() / 2];
    byte[] a = binario.bytesInvertidos(texto1);
    byte[] b = binario.bytesInvertidos(texto2);
    int indice = 0;
    for (int i = 0; i < a.length; i++) {
        cabecera[indice] = a[i];
        indice++;
    }
    for (int i = 0; i < b.length; i++) {
        cabecera[indice] = b[i];
        indice++;
    }
    try {
        codigo = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
        return "No existe";
    }
    byte[] hash = codigo.digest(codigo.digest(cabecera));
    StringBuilder cifrado = new StringBuilder();
    for (byte bit : hash) {
        cifrado.append(String.format("%02x", bit));
    }
    StringBuilder[] datos = new StringBuilder[cifrado.toString().length() / 2];
    for (int i = 0; i < datos.length; i++) {
        datos[i] = new StringBuilder();
    }
    int in = 0;
    for (int i = 0; i < datos.length; i++) {
        datos[i].append(cifrado.toString().charAt(in));
        in++;
        datos[i].append(cifrado.toString().charAt(in));
        in++;
    }
    StringBuilder invertir = new StringBuilder();
    for (int i = datos.length - 1; i >= 0; i--) {
        invertir.append(datos[i].toString());
    }
    return invertir.toString();
}

6.8 El timestamp

El timestamp son los segundos transcurridos desde el 1 de enero de 1970 a las 00:00:00 UTC. Bitcoin lo guarda en UTC, aunque algunos exploradores lo muestran en tu hora local (en Venezuela, GMT-4). Si trabajas con bloques viejos, recuerda que el país estuvo en GMT-4:30 antes de 2016: ese detalle puede descolocarte.

Programa Java para calcular el timestamp de un bloque
Generador de marca de tiempo en Java.

El código de este programa está en el repositorio que dejo en la sección de recursos, para no alargar más la entrada.

07. Probar con el bloque 709130

Estos son los datos del bloque que usé de ejemplo. Están en big-endian, tal como los muestra el explorador:

CampoValor
Versión3fffe004
Hash anterior00000000000000000000bfa3e0adec9293bfd9ce1919e8e141e61c99bc834421
Merkle root2150562a61f408a257a4a954f551d2325088cd7939befb89c130aa308bf3fceb
Timestamp2021-11-10 18:04:13 → 1636581853 → 618C41DD
Bits386727631 → 170CFECF
Nonce822226586 → 31022E9A
Hash resultado0000000000000000000aa425b68a7e425b01ec4fa966b42e330e13535b7ee947

Cómo reproducirlo

  1. Introduce los seis campos en el programa, en hexadecimal big-endian.
  2. Deja que el minero calcule el doble SHA-256 y compare con el objetivo.
  3. Si el hash no cumple, cambia el nonce y vuelve a intentar.
  4. Cuando lo encuentre, compara el resultado con el hash del explorador.
  5. Si coincide, tu implementación del protocolo está bien.

✅ Resultado: el hash generado es exactamente el del bloque 709130. Ahí es cuando ves que todo el rompecabezas encaja.

08. Errores comunes y consejos

  • El byte con signo: si un byte sale negativo, conviértelo con & 0xFF antes de pasarlo a hexadecimal. Es el error número uno.
  • Endianness: si el hash no coincide, revisa si invertiste los bytes de más o de menos. La cabecera va en little-endian; el resultado se muestra en big-endian.
  • El timestamp: usa la zona horaria correcta. Un desfase de horas cambia el hash por completo.
  • Comparaciones: usa compareTo con BigInteger, no ==.
  • El nonce se agota: en un bloque real, si se acaban las combinaciones hay que cambiar el timestamp o el CoinBase y volver a empezar.
  • No es para producción: esto es para aprender. Minar de verdad requiere hardware especializado y un pool.

09. Apoya el proyecto

He dedicado bastante tiempo a este tutorial y lo comparto gratis. Si te ha servido o te ha ahorrado horas de prueba y error, puedes apoyarme con una donación para seguir creando contenido sobre Bitcoin y programación. Gracias de verdad.

Bitcoin · SegWit

bc1qkmp5d8cxyza4yzhwsp0mm0pec5wzat7xd7n686

QR de la dirección Bitcoin SegWit

Bitcoin · Legacy

1EqsDHXRCG9FmpxkiJVFo5bTSfEGyD59AV

QR de la dirección Bitcoin Legacy

10. Recursos y referencias

Esto sería todo por ahora. Si encuentras un error o algo no te queda claro, déjalo en los comentarios y lo revisamos. Y si te sirvió, compártelo con alguien que esté aprendiendo Bitcoin por dentro.

Minar Bitcoin con Java desde cero: el protocolo crudo paso a paso

Bitcoin · Java · Protocolo crudo Minar Bitcoin con Java desde cero Sin APIs, sin librerías externas y sin el código fuente de Bitcoin. ...