Current section

Files

Jump to
bitcoinex CHANGELOG.md
Raw

CHANGELOG.md

# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.3.0] - 2026-08-04
### Added
- `PSBT.finalize/1` and `PSBT.finalized?/1` (Input Finalizer): assembles `final_scriptsig`/`final_scriptwitness` from the collected signatures and scripts for p2pkh, p2wpkh, (nested) p2sh-p2wpkh, bare/p2sh/p2wsh multisig, and p2sh-p2wsh inputs, ordering multisig signatures by the script's pubkey order. Best-effort per input (matching Bitcoin Core): inputs it cannot finalize are left untouched — including any whose `redeem_script`/`witness_script` does not hash to the scriptPubKey/witness program or whose single-key signature's pubkey does not hash to the p2pkh/p2wpkh hash (the BIP-174 Signer script/key-hash checks), and any non-witness spend type (p2pkh, bare/p2sh legacy multisig) without a `non_witness_utxo` matching the input's outpoint — a `witness_utxo` alone cannot be verified and never steers a non-witness finalization. When an input specifies a `sighash_type`, only signatures carrying that flag are eligible for selection; signatures with other flags (e.g. contributed for unrelated keys by another signer) do not block finalization. `finalized?/1` is false for a PSBT with no inputs.
- `PSBT.extract_tx/1` (Transaction Extractor): produces the fully-signed network `Transaction` from a finalized PSBT, or `{:error, :not_finalized}`.
- `PSBT.combine/2` (Combiner): merges two PSBTs describing the same unsigned transaction (compared byte-for-byte), unioning repeatable fields by key and requiring singleton fields to agree. Returns `{:error, :mismatched_tx}` for different transactions, `{:error, :missing_unsigned_tx}` when either side lacks one, `{:error, :map_count_mismatch}` for desynced input/output map lists, and `{:error, :conflicting_field}` on conflicting values. The union keeps the first PSBT's records in order and appends records new from the second (Bitcoin Core's merge semantics), so `combine/2` is idempotent, reproduces the official BIP-174 Combiner vector byte-for-byte, and is commutative up to record order.
- `PSBT.from_tx/1` (Creator), `PSBT.txid/1`, and the Updater API — `PSBT.add_global_field/3`, `PSBT.add_input_field/4`, `PSBT.add_output_field/4` (each dispatching on a field-name atom via `PSBT.Global.add_field/3`, `PSBT.In.add_field/3`, `PSBT.Out.add_field/3`). Adding a hash-preimage field (`:ripemd160`/`:sha256`/`:hash160`/`:hash256`) validates that the hash is the digest of the preimage (`{:error, :invalid_hash_preimage}` otherwise). `add_global_field(:unsigned_tx, ..)` validates the tx is unsigned and refuses to replace an existing one or desync the input/output map counts (`:unsigned_tx_already_set` / `:tx_io_count_mismatch`). `add_input_field(:witness_utxo/:final_scriptwitness, ..)` normalizes hex to lowercase and rejects non-hex, so the Updater never stores a value that would raise at encode time.
- PSBT parsing now covers the BIP-174 v0 input hash-preimage fields (`ripemd160`, `sha256`, `hash160`, `hash256`) and preserves unrecognized proprietary (`:proprietary`) and unknown (`:unknown`) key-value records on every map, so they round-trip.
- PSBT decoding now rejects malformed inputs per BIP-174: duplicate keys (`{:error, :duplicate_key}`), a known key type carrying wrong-length key data (`:invalid_key_format`), a wrong-length `sighash_type` or `version` value (`:invalid_sighash_type` / `:invalid_version`), a missing global unsigned tx (`:missing_unsigned_tx`), an unsigned tx with a non-empty scriptSig (`:unsigned_tx_has_script_sig`) or a witness/non-canonical serialization (`:unsigned_tx_not_canonically_serialized`), trailing bytes after the final output map (`:trailing_bytes`), and truncated/oversized data — instead of crashing or silently accepting.
- PSBT decoding rejects non-minimally encoded compact size lengths with `{:error, :non_canonical_compact_size}`, for both key and value lengths, as BIP-174 requires. These were previously accepted and re-serialized minimally, breaking byte-for-byte losslessness — and a non-minimally encoded *zero* key length produced an empty key that re-serialized as a map separator, so `decode |> encode_b64` silently emitted a different valid PSBT. **Breaking:** `PSBT.Utils.parse_compact_size_value/1` returns `{:ok, value, remaining}` instead of `{value, remaining}`; the untagged success tuple was shape-identical to its `{:error, reason}` tuple, so callers that did not check bound `value = :error` and the malformation surfaced under whatever error the resulting garbage happened to trip (`:invalid_psbt` for most fields).
- PSBT decoding rejects a `non_witness_utxo` whose transaction does not re-serialize to the exact value bytes (`{:error, :invalid_non_witness_utxo}`), mirroring the existing canonical-serialization check on the global unsigned tx.
- PSBT decoding also rejects trailing bytes *inside* a value payload — a `witness_utxo` with bytes after the scriptPubKey (`:invalid_witness_utxo`), a `final_scriptwitness` with bytes after the last stack item (`:invalid_final_scriptwitness`), and an `xpub`/`bip32_derivation` value whose path bytes are not whole 32-bit indexes (`:invalid_derivation`) — and, with the same errors, a `witness_utxo` or `final_scriptwitness` whose internal compact sizes are non-minimal, so the value could not re-serialize byte-for-byte. These were previously accepted and silently re-encoded differently, breaking losslessness.
- PSBT decoding and `add_global_field(:xpub, ..)` enforce the BIP-174 rule that a global xpub's derivation-path length must equal the extended key's depth byte (`{:error, :xpub_depth_mismatch}`).
- `Secp256k1.Point.parse_public_key/1` returns `{:error, "invalid public key"}` for a 33-byte key whose prefix byte is not `0x02`/`0x03` (previously raised `CaseClauseError`), so decoding a PSBT with such a pubkey in `partial_sig`/`bip32_derivation` returns a clean error instead of crashing the caller.
- `Transaction.Out.parse_output/1` and `Transaction.Witness.parse_witness/1`: strict single-item parsers that require the binary to hold exactly one serialized output/witness stack, returning `{:error, :invalid_output}` / `{:error, :invalid_witness}` for trailing bytes. PSBT uses these for the `PSBT_IN_WITNESS_UTXO` and `PSBT_IN_FINAL_SCRIPTWITNESS` record values, where an over-long value (stated length exceeding the actual output/witness) would otherwise re-encode to different bytes and break the `decode |> encode_b64` round-trip guarantee. The existing `output/1` and `witness/1` keep their lenient behavior of reading one item off the front of a longer binary, which is what whole-transaction parsing needs.
- PSBT decoding rejects 65-byte (uncompressed) public keys in `partial_sig`/`bip32_derivation` records with `{:error, :uncompressed_public_key}`. BIP-174 permits them, but bitcoinex cannot represent them faithfully (deliberate limitation, see the `Bitcoinex.PSBT` moduledoc).
- `PSBT.encode_b64/1` and `PSBT.to_file/2` return `{:error, :missing_unsigned_tx}` for a (hand-built) PSBT lacking the mandatory global unsigned transaction, instead of emitting a PSBT their own decoder rejects. **Breaking:** `encode_b64/1` therefore returns `{:ok, base64}` rather than a bare string, so callers destructure a result tuple instead of a `String.t() | {:error, term()}` union.
- `PSBT.from_tx/1` and `add_global_field(:unsigned_tx, ..)` accept an unsigned tx decoded from segwit serialization (one *empty* witness stack per input, as Bitcoin Core's Creator does) and normalize the empty stacks away; previously such a tx was rejected with `:tx_not_unsigned`.
- `add_input_field(:partial_sig, ..)` and `add_input_field(:non_witness_utxo, ..)` return `{:error, :invalid_partial_sig}` / `{:error, :invalid_non_witness_utxo}` for a truncated DER signature (one passing the `0x30` prefix check) or a utxo transaction whose hex fields cannot be serialized — both previously raised (`ArgumentError` / `MatchError`) out of the Updater API. `Signature.der_parse_signature/1` now parses with bounds-checked binary matching and never raises on malformed input.
- The Updater validates `:proprietary`/`:unknown` record keys (`{:error, :invalid_key_format}` for an empty key, a key whose type byte the map already parses into a dedicated field, an `:unknown` key carrying the `0xFC` proprietary type byte, or a `:proprietary` key without it) — previously such a record was accepted and the re-encoded PSBT either failed its own decode (`:duplicate_key`, or `:trailing_bytes` for an empty key) or silently re-decoded into a *different* struct (e.g. an `:unknown` `<<0x04>>` record migrating into `redeem_script`). It also bounds a `witness_utxo` amount to a uint64 (wider values silently truncated on encode), rejects a hand-built xpub whose `depth` is not a 1-byte binary instead of raising, and returns `{:error, :index_out_of_range}` / `{:error, :tx_io_count_mismatch}` instead of raising `ArgumentError` on a hand-built PSBT whose input/output map lists are `nil`.
### Changed
- **Breaking:** decoded PSBT fields are now typed `Bitcoinex` structs rather than hex/Base58 strings or integer lists: pubkeys are `Secp256k1.Point.t()`, scripts (`redeem_script`/`witness_script`/`final_scriptsig`) `Script.t()`, extended keys `ExtendedKey.t()`, and BIP-32 key origins a new `PSBT.KeyOrigin` struct (`fingerprint` as raw 4 bytes, `derivation` as `ExtendedKey.DerivationPath.t()`). `sighash_type` and global `version` are now integers. `partial_sig` is now a list (BIP-174 allows multiple signatures per input) of `%{public_key: Point.t(), signature: binary(), sighash_flag: integer()}`; the signature is kept as its **raw DER bytes** (validated as parseable DER, but not re-encoded) so that non-canonically-encoded signatures round-trip losslessly. Public keys in `partial_sig`/`bip32_derivation` must be 33-byte compressed SEC keys (the `Point` representation re-serializes compressed); legacy uncompressed 65-byte keys are rejected rather than silently re-encoded. Decoding rejects a `sighash_type` field or a `partial_sig` sighash flag outside the valid ECDSA set (`0x01`/`0x02`/`0x03` optionally `| 0x80`), a hash-preimage record whose key hash is not the digest of its value (`:invalid_hash_preimage`), an input `non_witness_utxo` whose txid or prevout index does not match its input's outpoint (`:non_witness_utxo_mismatch`), and any PSBT whose global version is not 0 (`:unsupported_version`) — only BIP-174 v0 is supported.
### Fixed
- `Transaction.Utils.serialize_compact_size_unsigned_int/1` now encodes values above `0xFFFFFFFF` as a `0xFF`-prefixed little-endian uint64. The final `cond` branch previously read `compact_size <= 0xFF` (unreachable), so any such value raised `CondClauseError`.
- PSBT (de)serialization is now lossless for all BIP-174 v0 fields. `PSBT.encode_b64/1` previously dropped the global `version`, per-input `por_commitment`, and any proprietary records (`Global.serialize_global/1` carried a `TODO: serialize all other fields`), so `decode |> encode_b64` was not the identity for many PSBTs.
- PSBT `partial_sig` records are repeatable per BIP-174 (keyed by pubkey); the previous single-map representation kept only the last record, silently discarding all but one signature from any multisig PSBT on decode.
- `Transaction` decoding now represents an input's empty witness stack as `%Witness{txinwitness: []}` (previously the integer `0`), which made re-serialization of any transaction containing one — e.g. a PSBT `non_witness_utxo` in segwit form with a mixed witness/non-witness input set — raise `Protocol.UndefinedError`.
- `Transaction.decode/1` now parses a 0-input transaction (whose `00 01` prefix collides with the segwit marker+flag) by falling back to legacy parsing when the segwit interpretation does not parse cleanly. PSBTs whose unsigned tx has 0 inputs (e.g. Bitcoin Core's `rpc_psbt.json` `valid[5]`) now decode and round-trip.
## [0.2.0] - 2026-07-10
### Changed
- BOLT11 invoice decoding now rejects invoices longer than 7089 characters with `{:error, :overall_max_length_exceeded}`, matching rust-lightning's limit (the capacity of the largest QR code). This bounds the work done decoding untrusted input now that all `r` (route hint) and `f` (fallback address) fields are parsed.
### Fixed
- BOLT11 invoice decoding now parses all `r` (route hint) fields instead of only the first. **Breaking:** `Invoice.route_hints` is now a list of route hints, each a list of `HopHint`s (`list(list(HopHint.t()))`), matching the BOLT11 spec where each `r` field is a separate private route. Empty `r` fields (invalid per BOLT11, which requires "one or more entries") are skipped.
- BOLT11 invoice decoding now parses all `f` (fallback address) fields instead of only the first, and correctly skips unknown-version and empty `f` fields without blocking later valid ones or failing the decode. **Breaking:** `Invoice.fallback_address` (a single address or `nil`) is replaced by `Invoice.fallback_addresses`, a list of addresses in order of preference (empty if none).
- BOLT11 `f` (fallback address) fields with version 17 (P2PKH) or 18 (P2SH) now require a 20-byte hash payload; any other length fails the decode with `:invalid_pubkey_hash_length` / `:invalid_script_hash_length` instead of encoding a garbage address.
- Removed an unreachable `Invoice.decode/1` clause that shadowed the `{:error, :no_ln_prefix}` error; the error is (and was) returned by HRP parsing inside the main clause, so behavior is unchanged.
### Removed
- The lnd-specific limit of 20 route hints per invoice (`{:error, :too_many_private_routes}`). BOLT11 places no limit on the number of `r` fields, so invoices with more than 20 route hints now decode successfully.
## [0.1.8] - 2024-03-01
### Added
- Fix warning emitted from String.slice with negative step when parsing amountful BOLT11 invoices.
## [0.1.7] - 2023-01-16
### Added
- Support for Schnorr signature creation and validation
- Fixed bug which would not correctly parse a BOLT11 invoice with amount explicitly set to 0.
- Bump Elixir & Erlang Requirements & dependencies
## [0.1.4] - 2021-05-19
### Added
- BIP32 support with new modules for extended keys and derivation paths.
- Security document for vulnerability reports.
- Extra test for PSBT with 0 inputs and 0 outputs.
### Changed
- Jason updated to 1.2.2
## [0.1.3] - 2021-04-19
### Added
- Disclaimer to README.
- Support for Bech32m.
- Private key module with signing functionality.
- hash160 added to utils.
### Changed
- Decimal dependency.
## [0.1.2] - 2021-01-13
### Added
- Code snippet examples to README.
- Padding function to utils.
### Fixed
- Padding to public keys and transaction IDs.
## [0.1.1] - 2020-12-21
### Added
- Native Elixir Secp256k1 elliptic curve support with ECDSA public key recovery.
### Removed
- libsecp25k1 and ex_doc dependencies.
## [0.1.0] - 2020-12-02
### Added
- Bech32 and base58 encoding.
- Address and lightning invoice serialization.
- PSBT serialization.
- Transaction module.
[0.3.0]: https://diff.hex.pm/diff/bitcoinex/0.2.0..0.3.0
[0.2.0]: https://diff.hex.pm/diff/bitcoinex/0.1.8..0.2.0
[0.1.4]: https://diff.hex.pm/diff/bitcoinex/0.1.3..0.1.4
[0.1.3]: https://diff.hex.pm/diff/bitcoinex/0.1.2..0.1.3
[0.1.2]: https://diff.hex.pm/diff/bitcoinex/0.1.1..0.1.2
[0.1.1]: https://diff.hex.pm/diff/bitcoinex/0.1.0..0.1.1
[0.1.0]: https://preview.hex.pm/preview/bitcoinex/0.1.0