---
slug: 38a-bitcoin
title: Bitcoin (la matriz UTXO — leer → construir → firmar → enviar → confirmar)
description: Operá sobre Bitcoin desde un agente con la misma seguridad estructural que el resto de la stack — direcciones y scripts (P2WPKH/P2TR/P2PKH, bech32/bech32m estrictos), el builder btc_tx donde cada satoshi está contabilizado (el fee implícito de UTXO se vuelve imposible de olvidar), firma Schnorr BIP-340 de taproot con la MISMA puerta `sign`, el lado de lectura Esplora (utxos, saldo, fees, broadcast, confirmación acotada), Bitcoin Core vía btc_rpc, y PSBT (BIP-174) para la custodia fría: el agente PREPARA la transacción, un humano la firma en su hardware wallet. La clave privada sellada como secret que nunca se materializa. Todo puro-Rust, un solo binario.
example_ids: [bitcoin]
---

# Bitcoin: cada satoshi contabilizado

Bitcoin no es una cadena de cuentas como Ethereum o Solana — es la **matriz UTXO**, y sus footguns son los más caros del ecosistema. El fee es **implícito** (inputs − outputs): olvidar el output de vuelto **dona todo el remanente a los mineros** (le pasó a gente real, con cientos de BTC). Se firma **una vez por input**, cada uno sobre un sighash distinto (BIP-143 segwit v0, BIP-341 taproot). Firmar sin ver los montos era posible hasta BIP-143.

Synsema convierte cada uno de esos footguns en un **error estructural imposible de cometer**. Y cierra la historia PSBT: **el agente prepara la transacción, el humano la firma en su hardware wallet** — custodia fría con agente autónomo, sin que la clave exista siquiera en la máquina del agente.

Hereda el modelo de seguridad de **[Blockchain](/es/0.6.x/38-blockchain)** — la clave es un `secret` que nunca se materializa, firmar es deny-by-default y auditado, y el lado de lectura pasa por la misma capability `net(host)` que HTTP. Bitcoin no agrega **ninguna** puerta de permiso nueva: `schnorr_sign` usa la MISMA capability `sign` que secp256k1/ed25519, `wif_import` usa `wallet`, el read-side usa `net`. Sólo firmar mueve valor.

```synsema
-- Bitcoin: the UTXO matrix — every satoshi accounted for (G-invariant), strict
-- addresses (BIP-350), Schnorr taproot behind the SAME `sign` gate, and PSBT for
-- cold custody. All PURE (no network): the read side (btc_utxos/btc_send/btc_wait)
-- is exercised against local mocks in the engine's test suite.

require sign("HOT")

test "addresses: BIP-84 P2WPKH and BIP-86 taproot (the tweak is internal)"
    -- The OFFICIAL vectors of bip-0084/bip-0086 (standard mnemonic, path 0).
    let pk be bytes("0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c", "hex")
    assert_eq(btc_address(pk), "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu")
    let xk be bytes("cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115", "hex")
    assert_eq(btc_address(xk, "p2tr"), "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr")
    -- Strict decode: checksum + the RIGHT bech32/bech32m variant (BIP-350), and
    -- hash160(pubkey) IS the program of its address.
    let d be btc_address_decode("bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu")
    assert_eq(d["kind"], "p2wpkh")
    assert_eq(d["network"], "mainnet")
    assert_eq(d["program"], hash160(pk))

test "every satoshi accounted for: the fee is DECLARED and the invariant balances"
    let ins be [{"txid": "0be2a795a30050f74e61f5ff5a16c7a5bca650e7a3af6a0ff04544821697b1cd",
        "vout": 0, "amount": 60000, "address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
        "pubkey": bytes("0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c", "hex")}]
    -- Balanced: 60000 in == 30000 + 29500 out + 500 fee. The change is one more
    -- EXPLICIT output back to your own address.
    let tx be btc_tx({"inputs": ins,
        "outputs": [{"address": "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", "amount": 30000},
                    {"address": "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu", "amount": 29500}],
        "fee": 500})
    assert_eq(tx["fee"], 500)
    assert_eq(tx["total_in"], 60000)
    assert_eq(length(tx["digests"]), 1)
    -- Forgetting the change output: the error names the EXACT difference — in
    -- real Bitcoin those 29500 sats would be silently donated to miners.
    let e be ""
    try
        let bad be btc_tx({"inputs": ins,
            "outputs": [{"address": "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", "amount": 30000}],
            "fee": 500})
    recover er
        set e to er
    assert(contains(e, "29500"))
    assert(contains(e, "change output"))
    -- Amounts are exact integer SATS: a float errors with the conversion.
    let e2 be ""
    try
        let bad be btc_tx({"inputs": ins,
            "outputs": [{"address": "bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el", "amount": 0.1}],
            "fee": 500})
    recover er
        set e2 to er
    assert(contains(e2, "100_000_000"))

test "taproot: sign per input with the internal tweak → assemble → the exact txid"
    -- The key of BIP-86 path 0 (standard mnemonic); the txid is pinned against an
    -- externally-built vector (embit) — Schnorr (BIP-340) is deterministic here.
    let k be as_secret("41f41d69260df4cf277826a9b65a3717e4eeddbeedf637f212ca096576479361", "HOT")
    let addr be btc_address(k, "p2tr")
    let tx be btc_tx({"inputs": [{"txid": "f0f4d6cee577621446b78b5b293cf2eee962766d682671101cf215edf20ba0a7",
        "vout": 0, "amount": 50000, "address": addr}],
        "outputs": [{"address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", "amount": 30000},
                    {"address": "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", "amount": 19700}],
        "fee": 300})
    -- "taproot" applies the BIP-341 key-path tweak INSIDE schnorr_sign.
    let sig be schnorr_sign(tx["digests"][0], k, "taproot")
    let raw be btc_tx_raw(tx, [sig])
    assert_eq(btc_txid(raw), "d98a84d0e281fd79a1b290a87ba19e8f434f392b5b2c47e60c7ea4556bedc3eb")

test "PSBT: the agent prepares and AUDITS; a human signs cold (pure, no key here)"
    let tx be btc_tx({"inputs": [{"txid": "f0f4d6cee577621446b78b5b293cf2eee962766d682671101cf215edf20ba0a7",
        "vout": 0, "amount": 50000,
        "address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"}],
        "outputs": [{"address": "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh", "amount": 30000},
                    {"address": "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", "amount": 19700}],
        "fee": 300})
    let psbt be psbt_encode(tx)          -- base64, importable in Sparrow/Ledger/…
    -- Round-trip: the audit view shows the implicit fee and every amount.
    let audit be psbt_decode(psbt)
    assert_eq(audit["fee"], 300)
    assert_eq(audit["total_out"], 49700)
    assert_eq(audit["complete"], false)  -- unsigned: the human hasn't signed yet

test "the gate is scoped: signing with an ungranted key name denies, catchable"
    -- `require sign("HOT")` covers HOT only — another label is denied (and inside
    -- a `sandbox` even HOT would be).
    let e be ""
    try
        let cold be as_secret("41f41d69260df4cf277826a9b65a3717e4eeddbeedf637f212ca096576479361", "COLD")
        let s be schnorr_sign(bytes("00000000000000000000000000000000000000000000000000000000000000ff", "hex"), cold, "taproot")
    recover er
        set e to er
    assert(contains(e, "sign"))
```

## El invariante central: G28 — cada satoshi contabilizado

En UTXO el fee no es un campo — es la **diferencia** entre lo que entra y lo que sale. `btc_tx` te obliga a declararlo y verifica el invariante `sum(inputs) == sum(outputs) + fee`. Si no cierra, el error nombra la diferencia exacta y dónde suele estar el bug:

```synsema
require sign("HOT")
let k be secret("HOT")
let pk be secp256k1_pubkey(k)

-- Olvidar el output de vuelto: 60000 de input, 50000 de salida, 500 de fee.
-- Faltan 9500 sats — que en Bitcoin real se DONAN a los mineros en silencio.
let tx be btc_tx({
    "inputs": [{"txid": txid_del_utxo, "vout": 0, "amount": 60000,
                "address": mi_direccion, "pubkey": pk}],
    "outputs": [{"address": destino, "amount": 50000}],
    "fee": 500})
-- ERROR: G28 violated — inputs (60000 sats) exceed outputs + fee (50500 sats)
--        by 9500 sats. Did you forget the change output? ...
```

El vuelto es **un output más**, explícito: se lo agregás a mano (Synsema no elige tus UTXOs ni tu change — la coin-selection es un pozo de privacidad; el caller decide). Con el output de vuelto, el invariante cierra y el builder devuelve los digests a firmar. El eco muestra `fee`, `vsize`, `total_in`, `total_out` y cada monto **antes** de firmar, para pasarlos por un `confirm`.

## El loop completo: leer → construir → firmar → enviar → confirmar

```synsema
require net("blockstream.info")
require sign("HOT")

let url be "https://blockstream.info/api"
let k be secret("HOT")
let mi_addr be btc_address(k)                     -- P2WPKH (BIP-84) por default

-- LEER: los UTXOs son la entrada directa del builder (cierra leer→construir)
let utxos be btc_utxos(url, mi_addr)              -- [{txid, vout, amount, confirmations}]
let fees be btc_fee_estimates(url)               -- {"1": sat/vB, "6": …} — números crudos
-- CONSTRUIR: cada satoshi contabilizado; el vuelto es un output explícito
let tx be btc_tx({
    "inputs": [{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"],
                "amount": utxos[0]["amount"], "address": mi_addr,
                "pubkey": secp256k1_pubkey(k)}],
    "outputs": [{"address": destino, "amount": 20000},
                {"address": mi_addr, "amount": utxos[0]["amount"] - 20000 - 500}],  -- vuelto
    "fee": 500})
-- MOSTRAR fee y montos ANTES de firmar (nada escondido en un blob)
let ok be confirm "¿Enviar 20000 sats, fee " + text(tx["fee"]) + "?" within 15m
when not ok
    give fail(403, "not approved")
-- FIRMAR (la ÚNICA puerta): una firma por input, en el mismo orden
let sig be secp256k1_sign(tx["digests"][0], k)
let raw be btc_tx_raw(tx, [sig])                 -- witness ensamblado (DER + SIGHASH_ALL)
-- ENVIAR y CONFIRMAR (acotado — una tx atascada jamás cuelga)
let txid be btc_send(url, raw)
let info be btc_wait(url, txid, 1, 600)          -- nothing al vencer
```

`btc_tx_raw` verifica **cada firma contra la clave del UTXO antes de ensamblar** — una firma con la clave equivocada jamás sale al aire. La firma la produce `secp256k1_sign` (ECDSA DER + low-s garantizado; vos nunca tocás DER). El `txid` que devuelve `btc_send` se re-chequea contra los bytes difundidos: si el nodo miente otro txid, es error.

## Taproot: firma Schnorr con el tweak interno

Gastar desde P2TR key-path (BIP-86) usa **Schnorr BIP-340**, con el tweak de taproot (BIP-341) aplicado **internamente** — vos nunca tweakeás una clave a mano (el error clásico de implementación):

```synsema
let k be secret("COLD")
let mi_addr be btc_address(k, "p2tr")            -- la dirección es la clave TWEAKED
let tx be btc_tx({
    "inputs": [{"txid": txid, "vout": 0, "amount": 50000, "address": mi_addr}],
    "outputs": [{"address": destino, "amount": 49700}],
    "fee": 300})
-- "taproot" aplica el tweak BIP-341 de key-path adentro de schnorr_sign
let sig be schnorr_sign(tx["digests"][0], k, "taproot")
let raw be btc_tx_raw(tx, [sig])
```

`schnorr_sign` usa la **misma** capability `sign` que secp256k1/ed25519 — cero puertas nuevas. Es determinista (aux-rand fijo en 32 bytes cero), byte-exacto contra los vectores oficiales de BIP-340/341. Sin el modo `"taproot"`, `schnorr_sign(digest, k)` firma BIP-340 plano (sin tweak).

## PSBT: el agente prepara, el humano firma en frío

La historia flagship de custodia: el agente arma la transacción y la exporta como **PSBT (BIP-174)**; un humano la firma en su **hardware wallet** (Ledger/Trezor/Coldcard/Sparrow) — la clave **jamás existió** en la máquina del agente — y el agente recibe el PSBT firmado, lo finaliza y lo difunde.

```synsema
-- El agente PREPARA (puro, sin `sign`: no hay clave acá)
let tx be btc_tx({"inputs": [...], "outputs": [...], "fee": 300})
let psbt be psbt_encode(tx)                      -- base64, importable en Sparrow/Ledger/…
-- ...el humano firma en frío y devuelve el PSBT firmado...
-- El agente AUDITA lo que va a difundir (nunca a ciegas)
let audit be psbt_decode(psbt_firmado)           -- {inputs, outputs, amounts, fee, complete}
let ok be confirm "¿Difundir? fee " + text(audit["fee"]) + " sats" within 15m
when ok
    let raw be psbt_finalize(psbt_firmado)       -- bytes de la tx firmada
    let txid be btc_send(url, raw)
```

`psbt_decode` te da el **fee implícito** del PSBT y cada monto/dirección para pasarlos por `show`/`confirm` antes de firmar o difundir un PSBT ajeno. Ninguna otra lib de agentes tiene esto de primera clase.

## Qué te da el lenguaje

| Builtin | Para qué | ¿Gateado? |
|---|---|---|
| `hash160(x)` | ripemd160(sha256(x)) → bytes(20), el hash de direcciones | puro |
| `btc_address(pubkey_o_secret, kind?, network?)` | dirección: `"p2wpkh"` (default) / `"p2tr"` / `"p2pkh"`; el tweak taproot es interno | puro |
| `btc_address_decode(text)` | `{kind, network, program, encoding}` — checksum + variante bech32/bech32m estrictos (BIP-350) | puro |
| `btc_script(address)` | el scriptPubKey de una dirección estándar → bytes | puro |
| `btc_txid(raw)` | dSHA256 sin witness, byte-reversed (la forma de exploradores/RPC) | puro |
| `schnorr_sign(digest32, secret, "taproot"?)` | firma BIP-340 → bytes(64); `"taproot"` aplica el tweak BIP-341 | **`require sign`** |
| `schnorr_verify(digest, sig64, xonly32)` / `schnorr_pubkey(secret)` | verificar / derivar la x-only pubkey | puro |
| `btc_tx(params)` | builder UTXO: `{digests: [uno por input], fee, vsize, + eco}`; G28 | puro |
| `btc_tx_raw(tx, signatures)` | la tx firmada (witness ensamblado; verifica cada firma) → bytes | puro |
| `psbt_encode(tx)` | PSBT unsigned (base64) desde el map de `btc_tx` | puro |
| `psbt_decode(text, network?)` | auditar un PSBT: inputs/outputs/amounts/fee/complete | puro |
| `psbt_finalize(text)` | bytes de la tx si el PSBT viene firmado de afuera | puro |
| `btc_utxos(url, address)` | los UTXOs de una dirección (Esplora) → lista lista para `btc_tx` | **`require net`** |
| `btc_balance(url, address)` | `{confirmed, mempool, total}` en sats exactos | **`require net`** |
| `btc_fee_estimates(url)` | objetivo-de-bloques → sat/vB (números crudos, no un oráculo) | **`require net`** |
| `btc_send(url, raw)` | broadcast → txid (re-chequeado contra los bytes) | **`require net`** |
| `btc_wait(url, txid, confirmations?, timeout?)` | espera de confirmación acotada (nothing al vencer) | **`require net`** |
| `btc_rpc(url, method, params?, auth?)` | JSON-RPC de Bitcoin Core; `auth.pass` puede ser un secret (Basic auth) | **`require net`** |
| `wif_import(text, label?)` | importar una clave WIF → `secret` (no existe el export inverso) | **`require wallet`** |

Las claves HD para Bitcoin salen de la misma custodia HD de **[Blockchain](/es/0.6.x/38-blockchain)**: `hd_derive(seed, "m/84'/0'/0'/0/0")` (BIP-84, P2WPKH) y `"m/86'/0'/0'/0/0"` (BIP-86, P2TR) alimentan `btc_address` sin materializar la clave.

## Instinto vs. realidad (leé esto antes de firmar nada)

| Tu instinto | La realidad |
|---|---|
| "el fee es un campo que pongo en la tx" | **Es implícito.** En UTXO el fee es `inputs − outputs`. `btc_tx` te obliga a declararlo y verifica `sum(inputs) == sum(outputs) + fee` (G28). Si no cierra, el error nombra la diferencia exacta. |
| "el vuelto lo calcula el builder" | **No.** El vuelto es **un output más**, explícito, a tu propia dirección. Olvidarlo dona el remanente a los mineros — por eso G28 lo atrapa antes de firmar. La coin-selection automática está fuera de alcance (la elegís vos). |
| "los montos los paso en BTC" | **No.** Todo es **sats enteros exactos**. 1 BTC = 100_000_000 sats. Un float (`0.1`) o un decimal se rechaza con la conversión en el error — nunca se adivina. |
| "firmo una vez la transacción" | **No.** Se firma **una vez por input**, cada uno sobre un sighash distinto. `btc_tx` devuelve `digests` (uno por input); pasás una firma por input a `btc_tx_raw`, en el mismo orden. |
| "el sighash es uno solo" | **No.** P2WPKH usa BIP-143 (segwit v0), P2TR usa BIP-341 (taproot) — algoritmos distintos. `btc_tx` elige el correcto según el tipo del UTXO. Sólo SIGHASH_ALL/DEFAULT (NONE/SINGLE/ANYONECANPAY son footguns de nicho, fuera de alcance). |
| "la dirección taproot es mi clave pública" | **No.** Es la clave **TWEAKED** (BIP-341 key-path). `btc_address(k, "p2tr")` aplica el tweak internamente; `schnorr_sign(…, "taproot")` firma con la clave tweakeada. Vos nunca tweakeás a mano — el error clásico de implementación. |
| "el txid lo leo tal cual de los bytes" | **No.** El txid es dSHA256 del serializado **sin witness**, **byte-reversed** para mostrar. `btc_txid` te da la forma que muestran exploradores y RPC — evita el clásico "mi txid está al revés". |
| "bech32 sirve para todas las segwit" | **No.** BIP-350: witness v0 (P2WPKH/P2WSH) usa **bech32**, v1+ (taproot) usa **bech32m**. Una dirección con la variante equivocada se **rechaza**. Decode laxo = fondos quemados. |
| "una dirección de testnet en una tx de mainnet, total es la misma clave" | **No.** Cross-red → error que **nombra las dos redes**. Un envío a la red equivocada quema fondos; `btc_tx` lo atrapa por estructura. |
| "r y s de la firma los pego crudos en el witness" | **No.** El witness P2WPKH lleva la firma en **DER + byte SIGHASH_ALL**; `btc_tx_raw` la DER-codifica por vos (low-s garantizado por k256). Vos nunca tocás DER. |
| "un fee más grande que lo que envío será lo que quise" | **Casi siempre un bug.** `fee > sum(outputs)` → error. Para el caso legítimo raro, `"allow_absurd_fee": true` (opt-in explícito, jamás silencioso). |
| "un output de 100 sats está bien" | **No.** Bajo el dust limit (546 P2PKH / 294 P2WPKH / 330 P2TR) no relaya — sats quemados. `btc_tx` lo atrapa nombrando el límite. |
| "una WIF la puedo exportar de vuelta" | **No.** `wif_import` existe (gateado por `wallet`); el export inverso **no** — ningún builtin devuelve una clave. El respaldo deliberado es `reveal()` del mnemónico. |
| "Bitcoin necesita su propio permiso de firma" | **No.** `schnorr_sign` usa la MISMA capability `sign` que secp256k1/ed25519; `wif_import` usa `wallet`; el read-side usa `net`. Cero puertas nuevas — `sign` sigue siendo la única que gasta. |

## Alcance

**Se gasta DESDE:** P2WPKH (BIP-84, bech32) y P2TR key-path (BIP-86, bech32m) — el presente y el futuro de las wallets. **Se envía HACIA:** cualquier tipo estándar (P2PKH, P2SH, P2WPKH, P2WSH, P2TR). **Redes:** `"mainnet"` (default), `"testnet"`, `"signet"`, `"regtest"`.

**Fuera de alcance (documentado, no es deuda):** gastar desde legacy P2PKH/P2SH/multisig/taproot script-path (enviar HACIA ellos sí funciona); sighash NONE/SINGLE/ANYONECANPAY (error dirigido si se piden); coin-selection automática (la elegís vos; patrón manual arriba); Lightning; Ordinals/inscriptions/BRC-20; descriptores de Core / xpub watch-only completo (PSBT ya cubre el flujo frío mínimo).
