bitcoin_wire/transaction

The transaction domain deserializes, inspects, validates, and serializes Bitcoin transactions while preserving Bitcoin’s wire representation.

Features

Quick Start

import bitcoin_wire/hash256
import bitcoin_wire/transaction
import gleam/result

pub fn display_txid_from_bytes(
  bytes: BitArray,
) -> Result(String, transaction.DecodeError) {
  bytes
  |> transaction.deserialize
  |> result.map(transaction.compute_txid)
  |> result.map(hash256.to_display_hex)
}

pub fn txid_bytes_from_hex(
  hex: String,
) -> Result(BitArray, transaction.DeserializeHexError) {
  hex
  |> transaction.deserialize_hex
  |> result.map(transaction.compute_txid)
  |> result.map(hash256.to_bytes_le)
}

Outpoint txids and computed txids and wtxids are exposed as Hash256 values in the same little-endian order used on the Bitcoin wire. Use hash256.to_display_hex for conventional explorer notation or hash256.to_bytes_le for the exact 32 wire-order bytes.

Output Values

Bitcoin’s wire format encodes output values as signed 64-bit integers. Consensus permits only non-negative amounts within Bitcoin’s money range. To model the wire format faithfully, OutputValue preserves the full signed range exactly on Erlang and JavaScript; validate_context_free_consensus checks the monetary constraints separately.

get_output_value returns an opaque OutputValue. Use the conversion helpers to display it, inspect its wire bytes, or attempt native integer conversion:

import bitcoin_wire/transaction
import gleam/int
import gleam/io

let value = transaction.get_output_value(output)
let wire_bytes = transaction.output_value_to_bytes_le(value)

case transaction.output_value_to_int(value) {
  Ok(satoshis) ->
    io.println("Native integer amount: " <> int.to_string(satoshis))
  Error(Nil) -> {
    let decimal_satoshis = transaction.output_value_to_string(value)
    io.println("Amount outside the native integer range: " <> decimal_satoshis)
  }
}

Decode Policy

deserialize and deserialize_hex apply these defaults:

LimitDefault
Serialized transaction size4,000,000 bytes
Input count100,000
Output count125,000
Total witness item count across all input stacks4,000,000

These defaults are permissive: their size and count ceilings accommodate canonical encodings of consensus-valid transactions under current Bitcoin limits. A transaction must fit within the 4,000,000-weight-unit block limit, and serialized byte size cannot exceed weight. Each witness item requires at least one byte. Inputs and outputs require at least 41 and 9 non-witness bytes, respectively, charged at four weight units per byte; their count ceilings exceed what can fit. See BIP 141 for the weight rules.

Serialized size does not directly bound heap usage: decoded collections can occupy much more memory than their wire bytes. Callers with tighter resource budgets can configure lower limits.

Use default_decode_policy and the decode_policy_with_* builders to customize them:

let policy =
  transaction.default_decode_policy()
  |> transaction.decode_policy_with_max_tx_size(1_000_000)
  |> transaction.decode_policy_with_max_input_count(5_000)
  |> transaction.decode_policy_with_max_witness_item_count(50_000)

let result = transaction.deserialize_with_policy(transaction_bytes, policy)

The size limit applies to the complete input buffer before decoding. Input and output count limits are checked before their collections are decoded, after checking whether the counts can fit in the remaining bytes. Script lengths, witness item counts, and witness item lengths must also fit the remaining input. The witness item limit applies to the cumulative count across all input stacks, including zero-length items; empty stacks contribute zero. Each stack’s count is checked against the remaining bytes, then against the cumulative limit before any of that stack’s items are decoded. A violation reports MaxWitnessItemCount with the cumulative count and points to the current stack’s item-count field.

Hex entry points enforce the same byte-size limit before allocating decoded bytes. Invalid hex takes precedence over size-policy errors, so oversized strings may still need to be scanned.

When transactions are decoded inside a block, the block’s serialized-size limit provides the byte envelope and max_tx_size is ignored. Each contained transaction still uses its configured input, output, and total witness item count limits.

These limits constrain decoding resources, not consensus validity. A parsed transaction has not passed script execution or context-free consensus checks, and a strict custom policy can reject consensus-valid transactions.

Context-Free Consensus Validation

Deserialization produces a Transaction(Parsed). Pass that transaction to validate_context_free_consensus to obtain a Transaction(ContextFreeValidated):

let assert Ok(validated_transaction) =
  transaction.validate_context_free_consensus(parsed_transaction)

On failure, Error(violations) contains a list of ConsensusViolation values. Independent violations are collected in deterministic validation order.

Scope

The module performs whole-value deserialization, structural inspection, serialization, output script classification, and documented context-free consensus checks. It does not perform full transaction validation requiring UTXO lookup, script execution, signature verification, block context, mempool policy, or network/RPC access.

Documentation

✨ Search Document