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:
Binario
Decimal
Hexadecimal
Octal
01011101
93
5D
135
11111111
255
FF
377
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.
Valor
Big-endian
Little-endian
1 byte
4D
4D
2 bytes
C78E
8EC7
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.
Campo
Bytes
Qué es
Versión
4
Versión del protocolo, en hexadecimal little-endian.
Hash anterior
32
Hash del bloque previo, en little-endian.
Merkle root
32
Hash de todas las transacciones del bloque, incluida la CoinBase.
Timestamp
4
Segundos desde el 1 de enero de 1970, 00:00:00 UTC.
Bits
4
Dificultad codificada; de aquí sale el objetivo.
Nonce
4
Nú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:
Concatenas los seis campos de la cabecera en little-endian.
Les aplicas doble SHA-256: SHA-256(SHA-256(cabecera)).
Comparas el resultado con el objetivo que sale del campo Bits.
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:
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:
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
privateinthexadecimalDecimal(String texto) {
int[] numero = newint[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
publicbyte[] bytesNoInvertidos(String texto) {
byte[] pares = newbyte[texto.length() / 2];
if (!(texto == "" || texto == " " || texto == null)) {
StringBuilder[] paresTexto = newStringBuilder[texto.length()/ 2];
for (int i = 0; i < paresTexto.length; i++) {
paresTexto[i] = newStringBuilder();
}
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
publicbyte[] bytesInvertidos(String texto) {
byte[] paresInvertidos = newbyte[texto.length() / 2];
StringBuilder[] paresTexto = newStringBuilder[texto.length() / 2];
for (int i = 0; i < paresTexto.length; i++) {
paresTexto[i] = newStringBuilder();
}
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 = newbyte[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;
publicStringcifrarTextoManualHex(String versionHexadecimal, String hashAnterirHexadecimal,
String MerkleRootHexadecimal, String tiempoHexadecimal, String tamanoBytesHexadecimal,
String nonceHexadecimal) {
byte[] cabecera = newbyte[versionHexadecimal.length() / 2 + hashAnterirHexadecimal.length() / 2
+ MerkleRootHexadecimal.length() / 2 + tiempoHexadecimal.length() / 2
+ tamanoBytesHexadecimal.length() / 2 + nonceHexadecimal.length() / 2];
byte[] version = newbyte[versionHexadecimal.length() / 2];
byte[] hashAnterior = newbyte[hashAnterirHexadecimal.length() / 2];
byte[] merkle = newbyte[MerkleRootHexadecimal.length() / 2];
byte[] tiempo = newbyte[tiempoHexadecimal.length() / 2];
byte[] tamanoBytes = newbyte[tamanoBytesHexadecimal.length() / 2];
byte[] nonce = newbyte[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 = newStringBuilder();
for (byte bit : hash) {
cifrado.append(String.format("%02x", bit));
}
StringBuilder[] datos = newStringBuilder[cifrado.toString().length() / 2];
for (int i = 0; i < datos.length; i++) {
datos[i] = newStringBuilder();
}
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 = newStringBuilder();
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
publicStringcifrarTextoManualHexValido(String versionHexadecimal, String hashAnterirHexadecimal,
String MerkleRootHexadecimal, String tiempoHexadecimal, String tamanoBytesHexadecimal) {
StringBuilder inv = newStringBuilder();
if (trabajo) {
StringBuilder invertir = newStringBuilder();
String nonceHexadecimal = binario.decimalHexadecimal(numeroDeTrabajo);
numeroDeTrabajo++;
byte[] cabecera = newbyte[versionHexadecimal.length() / 2 + hashAnterirHexadecimal.length() / 2
+ MerkleRootHexadecimal.length() / 2 + tiempoHexadecimal.length() / 2
+ tamanoBytesHexadecimal.length() / 2 + nonceHexadecimal.length() / 2];
byte[] version = newbyte[versionHexadecimal.length() / 2];
byte[] hashAnterior = newbyte[hashAnterirHexadecimal.length() / 2];
byte[] merkle = newbyte[MerkleRootHexadecimal.length() / 2];
byte[] tiempo = newbyte[tiempoHexadecimal.length() / 2];
byte[] tamanoBytes = newbyte[tamanoBytesHexadecimal.length() / 2];
byte[] nonce = newbyte[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 = newStringBuilder();
for (byte bit : hash) {
cifrado.append(String.format("%02x", bit));
}
StringBuilder[] datos = newStringBuilder[cifrado.toString().length() / 2];
for (int i = 0; i < datos.length; i++) {
datos[i] = newStringBuilder();
}
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 = newBigInteger(binario.hexadecimalDecimalGigante(objetivo));
BigInteger nume2 = newBigInteger(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
publicStringhexadecimalDecimalGigante(String textoHexadecimal) {
StringBuilder[] numeroDecimal = newStringBuilder[textoHexadecimal.length()];
for (int i = 0; i < textoHexadecimal.length(); i++) {
numeroDecimal[i] = newStringBuilder();
}
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 = newBigInteger[numeroDecimal.length];
BigInteger[] partes = newBigInteger[numeroDecimal.length];
BigInteger[] suma = newBigInteger[numeroDecimal.length - 1];
BigInteger base = newBigInteger("16");
for (int i = 0; i < numeros.length; i++) {
numeros[i] = newBigInteger(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
publicStringobtenerObjetivo(String bitHex) {
int exponente = hexadecimalDecimal8(bitHex) >> 24;
int numoroObtenido = hexadecimalDecimal8(bitHex) & 0xffffff;
BigInteger numeroGrande = newBigInteger("1");
int desplazar = (8 * (exponente - 3));
BigInteger numeroGrande2 = newBigInteger(String.valueOf(numoroObtenido));
BigInteger numeroGrande3 = numeroGrande.shiftLeft(desplazar);
BigInteger numero = numeroGrande2.multiply(numeroGrande3);
BigInteger base = newBigInteger("16");
int potencia = 0;
for (int i = 0; i < 1000; i++) {
if (base.pow(i).compareTo(numero) >= 1) {
potencia = i - 1;
break;
}
}
BigInteger[] division = newBigInteger[potencia];
BigInteger[] resto = newBigInteger[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 = newString[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 = newStringBuilder();
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");
} elseif (j == 11) {
numeroHex.append("B");
} elseif (j == 12) {
numeroHex.append("C");
} elseif (j == 13) {
numeroHex.append("D");
} elseif (j == 14) {
numeroHex.append("E");
} elseif (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
publicinthexadecimalDecimal8(String texto) {
int[] numero = newint[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
publicStringhexadecimalAhexadecimal64(String texto) {
String t = texto;
String tHex = "";
StringBuilder ceros = newStringBuilder();
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
publicStringobtenerMerkle(String[] textos) {
if (textos.length == 1) {
return textos[0];
}
ArrayList<String> textosHashLista = newArrayList<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 = newString[textosHashLista.size()];
for (int i = 0; i < textosHash.length; i++) {
textosHash[i] = textosHashLista.get(i);
}
returnobtenerMerkle(textosHash);
}
Si el número de hashes es impar, el último se repite. Y cada par se hashea así:
JavaTxHash.java
publicStringcifrarTransiciones(String texto1, String texto2) {
MessageDigest codigo = null;
byte[] cabecera = newbyte[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 = newStringBuilder();
for (byte bit : hash) {
cifrado.append(String.format("%02x", bit));
}
StringBuilder[] datos = newStringBuilder[cifrado.toString().length() / 2];
for (int i = 0; i < datos.length; i++) {
datos[i] = newStringBuilder();
}
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 = newStringBuilder();
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.
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:
Introduce los seis campos en el programa, en hexadecimal big-endian.
Deja que el minero calcule el doble SHA-256 y compare con el objetivo.
Si el hash no cumple, cambia el nonce y vuelve a intentar.
Cuando lo encuentre, compara el resultado con el hash del explorador.
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.
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.