Dos identificadores de una sola transacción
Una transacción Segregated Witness puede tener dos identificadores. El txid aplica el hash a la serialización tradicional sin el marcador, el indicador ni los campos testigo de SegWit. El wtxid aplica el hash a la serialización testigo completa. Ambos se refieren a la misma transacción, pero comprometen bytes distintos.
Esta guía analiza una transacción sintética pequeña y calcula ambos identificadores con la biblioteca estándar de Python. El ejemplo no es deliberadamente un gasto válido. Contiene una salida anterior inventada y elementos testigo arbitrarios, por lo que nunca debe transmitirse. Así podemos examinar la serialización sin una wallet, clave privada, nodo, conexión de red ni fondos.
BIP 141 define las dos serializaciones y sus identificadores. El código de transacciones de Bitcoin Core 31.1 serializa datos testigo solo cuando se solicitan y están presentes. Esas fuentes y la versión Bitcoin Core 31.1 se comprobaron el 9 de octubre de 2026. La versión 31.1 era la versión estable actual en esa fecha.
Requisitos y resultado esperado
Necesitas Python 3 y un editor de texto. No hace falta instalar paquetes ni conectarse a internet. Crea una carpeta descartable, guarda el programa siguiente como segwit_ids.py, revísalo y ejecuta:
python3 segwit_ids.py
El entorno probado usó Python 3.12.14. Una ejecución correcta informa de una serialización total de 69 bytes, una serialización base de 61 bytes y dos identificadores distintos:
txid=116102982e4852a9fe78c24f4c62e7b584378b956c83303d0d51b7b3b0fe6fb1
wtxid=8d195fe5fde6b872a64a027a8e33902a67cbdd802585e7a00d9dab19a072622a
También rechaza tres entradas mal formadas: una transacción truncada, un marcador incorrecto y un byte final adicional.
El programa completo
#!/usr/bin/env python3
from hashlib import sha256
RAW_HEX = (
"02000000"
"0001"
"01"
+ "11" * 32
+ "00000000"
"00"
"ffffffff"
"01"
"e803000000000000"
"01"
"51"
"02"
"01"
"01"
"02"
"0203"
"00000000"
)
class Reader:
def __init__(self, data):
self.data = data
self.offset = 0
def take(self, size):
end = self.offset + size
if end > len(self.data):
raise ValueError("truncated transaction")
part = self.data[self.offset:end]
self.offset = end
return part
def varint(self):
prefix = self.take(1)[0]
if prefix < 0xFD:
return prefix, bytes([prefix])
widths = {0xFD: 2, 0xFE: 4, 0xFF: 8}
body = self.take(widths[prefix])
value = int.from_bytes(body, "little")
minimum = {0xFD: 0xFD, 0xFE: 0x10000, 0xFF: 0x100000000}[prefix]
if value < minimum:
raise ValueError("non-canonical compact size")
return value, bytes([prefix]) + body
def copy_varbytes(reader):
size, encoded_size = reader.varint()
return encoded_size + reader.take(size)
def strip_witness(raw):
reader = Reader(raw)
base = bytearray(reader.take(4))
if reader.take(2) != b"\x00\x01":
raise ValueError("expected SegWit marker 00 and flag 01")
input_count, encoded = reader.varint()
if input_count == 0:
raise ValueError("transaction has no inputs")
base.extend(encoded)
for _ in range(input_count):
base.extend(reader.take(36))
base.extend(copy_varbytes(reader))
base.extend(reader.take(4))
output_count, encoded = reader.varint()
base.extend(encoded)
for _ in range(output_count):
base.extend(reader.take(8))
base.extend(copy_varbytes(reader))
for _ in range(input_count):
item_count, _ = reader.varint()
for _ in range(item_count):
copy_varbytes(reader)
base.extend(reader.take(4))
if reader.offset != len(raw):
raise ValueError("unexpected bytes after locktime")
return bytes(base)
def displayed_hash(payload):
return sha256(sha256(payload).digest()).digest()[::-1].hex()
if __name__ == "__main__":
raw = bytes.fromhex(RAW_HEX)
base = strip_witness(raw)
txid = displayed_hash(base)
wtxid = displayed_hash(raw)
assert len(raw) == 69
assert len(base) == 61
assert txid != wtxid
print(f"total_bytes={len(raw)}")
print(f"base_bytes={len(base)}")
print(f"base_hex={base.hex()}")
print(f"txid={txid}")
print(f"wtxid={wtxid}")
invalid = {
"truncated": raw[:-1],
"wrong marker": raw[:4] + b"\x01" + raw[5:],
"trailing byte": raw + b"\x00",
}
for label, candidate in invalid.items():
try:
strip_witness(candidate)
except ValueError as error:
print(f"PASS rejected {label}: {error}")
else:
raise AssertionError(f"FAIL accepted {label}")
Lee el ejemplo en el orden del protocolo
Los primeros cuatro bytes, 02000000, codifican la versión 2 de la transacción en orden little-endian. Los dos bytes siguientes son el marcador y el indicador SegWit, 00 01. Anuncian la serialización extendida. Se incluyen al calcular el wtxid, pero se omiten al calcular el txid.
El siguiente 01 indica que hay una entrada. El hash de su transacción anterior consta de 32 bytes 11 repetidos. Los cuatro bytes siguientes seleccionan el índice de salida cero. Un valor compact-size de cero significa que scriptSig está vacío y ffffffff es la secuencia. Estos valores son solo datos estructurales de prueba. No identifican una moneda gastable.
El siguiente 01 indica que hay una salida. e803000000000000 son 1.000 satoshis codificados como entero sin signo de 64 bits en little-endian. El script de salida de un byte es 51, el código de operación OP_TRUE. De nuevo, es un ejemplo para el analizador, no un pago.
El testigo empieza con 02, que indica dos elementos en la pila. El primero tiene un byte, 01. El segundo tiene dos bytes, 0203. Los cuatro bytes cero finales son el locktime.
Construye la serialización base de forma segura
El programa no elimina patrones de bytes con un reemplazo de texto. Sería inseguro porque 0001 o bytes parecidos al testigo pueden aparecer en otros lugares. Reader avanza por la transacción según los límites de cada campo.
Los enteros compact-size determinan el número de entradas, salidas y elementos testigo, además de las longitudes de scripts y elementos. varint también rechaza codificaciones largas de valores pequeños. copy_varbytes mantiene cada prefijo de longitud unido a su contenido cuando el campo pertenece a la serialización base.
strip_witness copia la versión, las entradas, las salidas y el locktime. Lee, pero no copia, el marcador, el indicador ni el testigo. Rechaza un número de entradas igual a cero, campos truncados y cualquier byte después del locktime. Los 61 bytes resultantes son:
020000000111111111111111111111111111111111111111111111111111111111111111110000000000ffffffff01e803000000000000015100000000
Aplica el hash y muestra los identificadores
Bitcoin usa SHA-256 dos veces para ambos identificadores. Por convención, los bytes del hash se muestran en el orden inverso al resumen de 32 bytes que devuelve la función. Por eso displayed_hash invierte el resumen antes de convertirlo a hexadecimal.
El txid pasa la serialización base de 61 bytes a displayed_hash. El wtxid pasa los 69 bytes. La diferencia de ocho bytes corresponde al marcador y el indicador de dos bytes más seis bytes de serialización testigo. Cambiar solo un elemento testigo cambia el wtxid, pero deja intacto el txid.
El resultado se volvió a calcular de forma independiente con Node.js 24.19.0, usando su módulo integrado crypto y una serialización base construida de forma explícita. Ambas implementaciones produjeron los mismos dos identificadores y números de bytes.
Comprueba la salida completa probada
total_bytes=69
base_bytes=61
base_hex=020000000111111111111111111111111111111111111111111111111111111111111111110000000000ffffffff01e803000000000000015100000000
txid=116102982e4852a9fe78c24f4c62e7b584378b956c83303d0d51b7b3b0fe6fb1
wtxid=8d195fe5fde6b872a64a027a8e33902a67cbdd802585e7a00d9dab19a072622a
PASS rejected truncated: truncated transaction
PASS rejected wrong marker: expected SegWit marker 00 and flag 01
PASS rejected trailing byte: unexpected bytes after locktime
El SHA-256 del código Python fue 8edaaa4bfd72dea2be37a98cceeadc2ce4fd74f02b38fc591cb50de053570e08. El SHA-256 de la salida completa fue ee3d0f4e394b0a7aa634935f9eab38d26720f326ab556737c492210faaa7b190. Estos hashes identifican el ejemplo local exacto y la salida probada. No convierten en fiable un código copiado.
Solución de problemas
Los identificadores aparecen invertidos: invierte los bytes solo después del segundo SHA-256. Invertir los bytes de la transacción o hacerlo después de cada hash calcula otra cosa.
El txid coincide con el wtxid: es normal en una transacción sin datos testigo. En este ejemplo, la igualdad significa probablemente que se aplicó el hash dos veces a la serialización completa o que no se retiraron el marcador, el indicador y el testigo de la entrada del txid.
El analizador informa de un truncamiento: comprueba cada longitud compact-size y el locktime final. Un solo byte ausente puede hacer que una longitud posterior consuma el campo equivocado.
Una transacción real usa el marcador 00 con otro indicador: este analizador didáctico solo acepta el indicador 01 definido actualmente. Usa software Bitcoin mantenido para datos generales de red, en vez de ampliar el ejemplo mediante suposiciones.
Un explorador muestra un solo identificador: el txid sigue siendo la referencia habitual en exploradores y wallets. El wtxid es importante para la retransmisión consciente del testigo y los compromisos de bloque, pero muchas interfaces no lo muestran.
Límites importantes
Calcular un identificador no valida scripts, firmas, importes, reglas de consenso ni la existencia de las salidas referenciadas. Este ejemplo no es deliberadamente un gasto válido. No lo transmitas ni lo adaptes insertando una salida anterior o un testigo reales.
El programa gestiona el marcador y el indicador SegWit actuales y analiza longitudes compact-size, pero es una comprobación didáctica, no un sustituto del decodificador de transacciones de Bitcoin Core. BIP 141 también asigna a la transacción coinbase un wtxid especial con todos los bytes a cero al construir el compromiso testigo. Aplicar esta función al hash de una serialización coinbase no implementa esa regla separada.
Usa el ejercicio para ver qué bytes compromete cada identificador. En software de producción, utiliza una biblioteca Bitcoin mantenida, vectores de prueba oficiales y una revisión que abarque conjuntamente el análisis, la validación de consenso y la gestión de errores.
